Files
2026-07-13 13:39:12 +08:00

193 lines
6.1 KiB
TypeScript

import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
registerHook,
unregisterHooks,
emitHookBlocking,
emitHook,
runOnRequest,
runOnResponse,
runOnError,
resetHooks,
getHooks,
} from "../../src/lib/plugins/hooks.ts";
beforeEach(() => {
resetHooks();
});
const makeCtx = () => ({
requestId: `req-${Date.now()}`,
body: { model: "gpt-4", messages: [] },
model: "gpt-4",
provider: "openai",
metadata: {},
});
describe("registerHook", () => {
it("registers a hook for an event", () => {
registerHook("onRequest", "test-plugin", () => {});
const list = getHooks("onRequest");
assert.equal(list.length, 1);
assert.equal(list[0].pluginName, "test-plugin");
assert.equal(list[0].priority, 100);
});
it("sorts hooks by priority", () => {
registerHook("onRequest", "low", () => {}, 200);
registerHook("onRequest", "high", () => {}, 10);
registerHook("onRequest", "mid", () => {}, 100);
const list = getHooks("onRequest");
assert.equal(list[0].pluginName, "high");
assert.equal(list[1].pluginName, "mid");
assert.equal(list[2].pluginName, "low");
});
it("prevents duplicate registration", () => {
const handler = () => {};
registerHook("onRequest", "p1", handler);
registerHook("onRequest", "p1", handler);
const list = getHooks("onRequest");
assert.equal(list.length, 1);
});
it("allows same plugin with different handlers", () => {
registerHook("onRequest", "p1", () => {});
registerHook("onRequest", "p1", () => {});
const list = getHooks("onRequest");
assert.equal(list.length, 2);
});
});
describe("unregisterHooks", () => {
it("removes all hooks for a plugin", () => {
registerHook("onRequest", "p1", () => {});
registerHook("onResponse", "p1", () => {});
registerHook("onRequest", "p2", () => {});
unregisterHooks("p1");
assert.equal(getHooks("onRequest").length, 1);
assert.equal(getHooks("onResponse").length, 0);
});
});
describe("emitHookBlocking", () => {
it("returns empty when no hooks registered", async () => {
const result = await emitHookBlocking("onRequest", makeCtx());
assert.equal(result.blocked, undefined);
});
it("blocks request when hook returns blocked", async () => {
registerHook("onRequest", "blocker", () => ({
blocked: true,
response: { error: "denied" },
}));
const result = await emitHookBlocking("onRequest", makeCtx());
assert.equal(result.blocked, true);
assert.deepEqual(result.response, { error: "denied" });
});
it("chains body/metadata through hooks", async () => {
registerHook("onRequest", "p1", (payload: any) => ({
body: { ...payload.body, added: true },
metadata: { p1: true },
}), 10);
registerHook("onRequest", "p2", (payload: any) => ({
metadata: { ...payload.metadata, p2: true },
}), 20);
const result = await emitHookBlocking("onRequest", makeCtx());
assert.equal(result.blocked, undefined);
assert.equal((result.body as any).added, true);
assert.deepEqual(result.metadata, { p1: true, p2: true });
});
it("swallows hook errors", async () => {
registerHook("onRequest", "p1", () => { throw new Error("boom"); });
const result = await emitHookBlocking("onRequest", makeCtx());
assert.equal(result.blocked, undefined);
});
});
describe("runOnRequest", () => {
it("returns not blocked when no hooks", async () => {
const result = await runOnRequest(makeCtx());
assert.equal(result.blocked, undefined);
});
it("blocks request when hook returns blocked", async () => {
registerHook("onRequest", "blocker", () => ({
blocked: true,
response: { error: "denied" },
}));
const result = await runOnRequest(makeCtx());
assert.equal(result.blocked, true);
assert.deepEqual(result.response, { error: "denied" });
});
it("chains body/metadata through hooks", async () => {
registerHook("onRequest", "p1", (payload: any) => ({
body: { ...payload.body, added: true },
metadata: { p1: true },
}), 10);
registerHook("onRequest", "p2", (payload: any) => ({
metadata: { ...payload.metadata, p2: true },
}), 20);
const result = await runOnRequest(makeCtx());
assert.equal(result.blocked, undefined);
assert.equal((result.body as any).added, true);
assert.deepEqual(result.metadata, { p1: true, p2: true });
});
it("swallows hook errors", async () => {
registerHook("onRequest", "p1", () => { throw new Error("boom"); });
const result = await runOnRequest(makeCtx());
assert.equal(result.blocked, undefined);
});
});
describe("runOnResponse", () => {
it("returns original response when no hooks", async () => {
const resp = { choices: [{ message: { content: "hi" } }] };
const result = await runOnResponse(makeCtx(), resp);
assert.deepEqual(result, resp);
});
it("chains response through hooks", async () => {
registerHook("onResponse", "p1", (payload: any) => ({
response: { ...payload.response, p1: true },
}));
registerHook("onResponse", "p2", (payload: any) => ({
response: { ...payload.response, p2: true },
}));
const result = await runOnResponse(makeCtx(), { original: true });
assert.deepEqual(result, { original: true, p1: true, p2: true });
});
it("swallows hook errors", async () => {
registerHook("onResponse", "p1", () => { throw new Error("boom"); });
const result = await runOnResponse(makeCtx(), { original: true });
assert.deepEqual(result, { original: true });
});
});
describe("runOnError", () => {
it("fires error hooks", async () => {
let caught = false;
registerHook("onError", "p1", (payload: any) => {
caught = true;
});
await runOnError(makeCtx(), new Error("test"));
assert.equal(caught, true);
});
});
describe("resetHooks", () => {
it("clears all hooks", () => {
registerHook("onRequest", "p1", () => {});
registerHook("onResponse", "p2", () => {});
resetHooks();
assert.equal(getHooks("onRequest").length, 0);
assert.equal(getHooks("onResponse").length, 0);
});
});