openclaw/src/hooks/loader.test.ts

264 lines
7.6 KiB
TypeScript
Raw Normal View History

2026-01-17 01:55:42 +00:00
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
2026-01-17 01:55:42 +00:00
import {
clearInternalHooks,
getRegisteredEventKeys,
triggerInternalHook,
createInternalHookEvent,
} from "./internal-hooks.js";
import { loadInternalHooks } from "./loader.js";
2026-01-17 01:55:42 +00:00
describe("loader", () => {
2026-01-17 01:31:39 +00:00
let tmpDir: string;
let originalBundledDir: string | undefined;
beforeEach(async () => {
clearInternalHooks();
// Create a temp directory for test modules
2026-01-30 03:15:10 +01:00
tmpDir = path.join(os.tmpdir(), `openclaw-test-${Date.now()}`);
2026-01-17 01:31:39 +00:00
await fs.mkdir(tmpDir, { recursive: true });
// Disable bundled hooks during tests by setting env var to non-existent directory
2026-01-30 03:15:10 +01:00
originalBundledDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks";
2026-01-17 01:31:39 +00:00
});
afterEach(async () => {
clearInternalHooks();
// Restore original env var
if (originalBundledDir === undefined) {
2026-01-30 03:15:10 +01:00
delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
2026-01-17 01:31:39 +00:00
} else {
2026-01-30 03:15:10 +01:00
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = originalBundledDir;
2026-01-17 01:31:39 +00:00
}
// Clean up temp directory
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
2026-01-17 01:55:42 +00:00
describe("loadInternalHooks", () => {
it("should return 0 when hooks are not enabled", async () => {
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: false,
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
});
2026-01-17 01:55:42 +00:00
it("should return 0 when hooks config is missing", async () => {
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {};
2026-01-17 01:31:39 +00:00
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
});
2026-01-17 01:55:42 +00:00
it("should load a handler from a module", async () => {
2026-01-17 01:31:39 +00:00
// Create a test handler module
2026-01-17 01:55:42 +00:00
const handlerPath = path.join(tmpDir, "test-handler.js");
2026-01-17 01:31:39 +00:00
const handlerCode = `
export default async function(event) {
// Test handler
}
`;
2026-01-17 01:55:42 +00:00
await fs.writeFile(handlerPath, handlerCode, "utf-8");
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
module: path.basename(handlerPath),
2026-01-17 01:31:39 +00:00
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
const keys = getRegisteredEventKeys();
2026-01-17 01:55:42 +00:00
expect(keys).toContain("command:new");
2026-01-17 01:31:39 +00:00
});
2026-01-17 01:55:42 +00:00
it("should load multiple handlers", async () => {
2026-01-17 01:31:39 +00:00
// Create test handler modules
2026-01-17 01:55:42 +00:00
const handler1Path = path.join(tmpDir, "handler1.js");
const handler2Path = path.join(tmpDir, "handler2.js");
2026-01-17 01:31:39 +00:00
2026-01-17 01:55:42 +00:00
await fs.writeFile(handler1Path, "export default async function() {}", "utf-8");
await fs.writeFile(handler2Path, "export default async function() {}", "utf-8");
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{ event: "command:new", module: path.basename(handler1Path) },
{ event: "command:stop", module: path.basename(handler2Path) },
2026-01-17 01:31:39 +00:00
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(2);
const keys = getRegisteredEventKeys();
2026-01-17 01:55:42 +00:00
expect(keys).toContain("command:new");
expect(keys).toContain("command:stop");
2026-01-17 01:31:39 +00:00
});
2026-01-17 01:55:42 +00:00
it("should support named exports", async () => {
2026-01-17 01:31:39 +00:00
// Create a handler module with named export
2026-01-17 01:55:42 +00:00
const handlerPath = path.join(tmpDir, "named-export.js");
2026-01-17 01:31:39 +00:00
const handlerCode = `
export const myHandler = async function(event) {
// Named export handler
}
`;
2026-01-17 01:55:42 +00:00
await fs.writeFile(handlerPath, handlerCode, "utf-8");
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
module: path.basename(handlerPath),
2026-01-17 01:55:42 +00:00
export: "myHandler",
2026-01-17 01:31:39 +00:00
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
});
2026-01-17 01:55:42 +00:00
it("should handle module loading errors gracefully", async () => {
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
module: "missing-handler.js",
2026-01-17 01:31:39 +00:00
},
],
},
},
};
// Should not throw and should return 0 (handler failed to load)
2026-01-17 01:31:39 +00:00
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
});
2026-01-17 01:55:42 +00:00
it("should handle non-function exports", async () => {
2026-01-17 01:31:39 +00:00
// Create a module with a non-function export
2026-01-17 01:55:42 +00:00
const handlerPath = path.join(tmpDir, "bad-export.js");
await fs.writeFile(handlerPath, 'export default "not a function";', "utf-8");
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
module: path.basename(handlerPath),
2026-01-17 01:31:39 +00:00
},
],
},
},
};
// Should not throw and should return 0 (handler is not a function)
2026-01-17 01:31:39 +00:00
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
});
2026-01-17 01:55:42 +00:00
it("should handle relative paths", async () => {
2026-01-17 01:31:39 +00:00
// Create a handler module
2026-01-17 01:55:42 +00:00
const handlerPath = path.join(tmpDir, "relative-handler.js");
await fs.writeFile(handlerPath, "export default async function() {}", "utf-8");
2026-01-17 01:31:39 +00:00
// Relative to workspaceDir (tmpDir)
const relativePath = path.relative(tmpDir, handlerPath);
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
2026-01-17 01:31:39 +00:00
module: relativePath,
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
});
2026-01-17 01:55:42 +00:00
it("should actually call the loaded handler", async () => {
2026-01-17 01:31:39 +00:00
// Create a handler that we can verify was called
2026-01-17 01:55:42 +00:00
const handlerPath = path.join(tmpDir, "callable-handler.js");
2026-01-17 01:31:39 +00:00
const handlerCode = `
let callCount = 0;
export default async function(event) {
callCount++;
}
export function getCallCount() {
return callCount;
}
`;
2026-01-17 01:55:42 +00:00
await fs.writeFile(handlerPath, handlerCode, "utf-8");
2026-01-17 01:31:39 +00:00
2026-01-30 03:15:10 +01:00
const cfg: OpenClawConfig = {
2026-01-17 01:31:39 +00:00
hooks: {
internal: {
enabled: true,
handlers: [
{
2026-01-17 01:55:42 +00:00
event: "command:new",
module: path.basename(handlerPath),
2026-01-17 01:31:39 +00:00
},
],
},
},
};
await loadInternalHooks(cfg, tmpDir);
// Trigger the hook
2026-01-17 01:55:42 +00:00
const event = createInternalHookEvent("command", "new", "test-session");
2026-01-17 01:31:39 +00:00
await triggerInternalHook(event);
// The handler should have been called, but we can't directly verify
// the call count from this context without more complex test infrastructure
// This test mainly verifies that loading and triggering doesn't crash
2026-01-17 01:55:42 +00:00
expect(getRegisteredEventKeys()).toContain("command:new");
2026-01-17 01:31:39 +00:00
});
});
});