Files
openclaw/src/plugins/wired-hooks-gateway.test.ts
Alex Knight f155a5f955 Add cron changed plugin hook (#72773)
* feat: add cron changed plugin hook

* fix: improve cron_changed hook correctness and code quality

- Fix PluginHookGatewayCronDeliveryStatus: replace 'error' with 'unknown'
  to match internal CronDeliveryStatus enum
- Add job snapshot to CronEvent so removed events carry the deleted job
- Extract pickDefined helper, replace 14-field verbose spread mapping
- Add toPluginCronJob mapper for explicit internal→public type boundary
- Fix schedule union: use literal-only kind discriminants for TS narrowing
- Use loadConfig() (runtime) instead of params.cfg (startup) in hook ctx
- Use formatErrorMessage instead of String(err) for stack preservation
- Fix pre-existing getCron TS2322 with explicit cast (matches gateway_start)
- Re-export supporting types from hooks.ts for plugin consumers
- Add tests: removed events with job, finished with full fields, runtime cfg
2026-04-28 21:34:42 +10:00

129 lines
4.1 KiB
TypeScript

/**
* Test: gateway_start & gateway_stop hook wiring (server.impl.ts)
*
* Since startGatewayServer is heavily integrated, we test the hook runner
* calls at the unit level by verifying the hook runner functions exist
* and validating the integration pattern.
*/
import { describe, expect, it, vi } from "vitest";
import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js";
import type {
PluginHookCronChangedEvent,
PluginHookGatewayContext,
PluginHookGatewayStartEvent,
PluginHookGatewayStopEvent,
} from "./types.js";
async function expectGatewayHookCall(params: {
hookName: "gateway_start" | "gateway_stop";
event: PluginHookGatewayStartEvent | PluginHookGatewayStopEvent;
gatewayCtx: PluginHookGatewayContext;
}) {
const handler = vi.fn();
const { runner } = createHookRunnerWithRegistry([{ hookName: params.hookName, handler }]);
if (params.hookName === "gateway_start") {
await runner.runGatewayStart(params.event as PluginHookGatewayStartEvent, params.gatewayCtx);
} else {
await runner.runGatewayStop(params.event as PluginHookGatewayStopEvent, params.gatewayCtx);
}
expect(handler).toHaveBeenCalledWith(params.event, params.gatewayCtx);
}
describe("gateway hook runner methods", () => {
const gatewayCtx = {
port: 18789,
config: {} as never,
workspaceDir: "/tmp/openclaw-workspace",
getCron: () => undefined,
};
it.each([
{
name: "runGatewayStart invokes registered gateway_start hooks",
hookName: "gateway_start" as const,
event: { port: 18789 },
},
{
name: "runGatewayStop invokes registered gateway_stop hooks",
hookName: "gateway_stop" as const,
event: { reason: "test shutdown" },
},
] as const)("$name", async ({ hookName, event }) => {
await expectGatewayHookCall({ hookName, event, gatewayCtx });
});
it("runCronChanged invokes registered cron_changed hooks", async () => {
const handler = vi.fn();
const { runner } = createHookRunnerWithRegistry([{ hookName: "cron_changed", handler }]);
const event: PluginHookCronChangedEvent = {
action: "updated",
jobId: "job-1",
nextRunAtMs: 123,
job: {
id: "job-1",
state: { nextRunAtMs: 123 },
},
};
await runner.runCronChanged(event, gatewayCtx);
expect(handler).toHaveBeenCalledWith(event, gatewayCtx);
});
it("runCronChanged passes finished events with delivery and error fields", async () => {
const handler = vi.fn();
const { runner } = createHookRunnerWithRegistry([{ hookName: "cron_changed", handler }]);
const event: PluginHookCronChangedEvent = {
action: "finished",
jobId: "job-2",
status: "error",
error: "timeout",
summary: "Job timed out",
delivered: false,
deliveryStatus: "not-delivered",
deliveryError: "channel unavailable",
durationMs: 5000,
runAtMs: 100,
nextRunAtMs: 200,
model: "gpt-5.4",
provider: "openai",
job: {
id: "job-2",
state: { lastRunStatus: "error", lastError: "timeout" },
},
};
await runner.runCronChanged(event, gatewayCtx);
expect(handler).toHaveBeenCalledWith(event, gatewayCtx);
});
it("runCronChanged handles removed events without job", async () => {
const handler = vi.fn();
const { runner } = createHookRunnerWithRegistry([{ hookName: "cron_changed", handler }]);
const event: PluginHookCronChangedEvent = {
action: "removed",
jobId: "job-3",
job: { id: "job-3", name: "deleted-job" },
};
await runner.runCronChanged(event, gatewayCtx);
expect(handler).toHaveBeenCalledWith(event, gatewayCtx);
expect(handler.mock.calls[0][0].job).toEqual({ id: "job-3", name: "deleted-job" });
});
it("hasHooks returns true for registered gateway hooks", () => {
const { runner } = createHookRunnerWithRegistry([
{ hookName: "gateway_start", handler: vi.fn() },
{ hookName: "cron_changed", handler: vi.fn() },
]);
expect(runner.hasHooks("gateway_start")).toBe(true);
expect(runner.hasHooks("cron_changed")).toBe(true);
expect(runner.hasHooks("gateway_stop")).toBe(false);
});
});