mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 11:00:50 +00:00
271 lines
7.7 KiB
TypeScript
271 lines
7.7 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
clearInternalHooks,
|
|
getRegisteredEventKeys,
|
|
triggerInternalHook,
|
|
createInternalHookEvent,
|
|
} from "./internal-hooks.js";
|
|
import { loadInternalHooks } from "./loader.js";
|
|
|
|
describe("loader", () => {
|
|
let fixtureRoot = "";
|
|
let caseId = 0;
|
|
let tmpDir: string;
|
|
let originalBundledDir: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hooks-loader-"));
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
clearInternalHooks();
|
|
// Create a temp directory for test modules
|
|
tmpDir = path.join(fixtureRoot, `case-${caseId++}`);
|
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
|
|
// Disable bundled hooks during tests by setting env var to non-existent directory
|
|
originalBundledDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
|
|
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks";
|
|
});
|
|
|
|
afterEach(async () => {
|
|
clearInternalHooks();
|
|
// Restore original env var
|
|
if (originalBundledDir === undefined) {
|
|
delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
|
|
} else {
|
|
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = originalBundledDir;
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (!fixtureRoot) {
|
|
return;
|
|
}
|
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("loadInternalHooks", () => {
|
|
it("should return 0 when hooks are not enabled", async () => {
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it("should return 0 when hooks config is missing", async () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it("should load a handler from a module", async () => {
|
|
// Create a test handler module
|
|
const handlerPath = path.join(tmpDir, "test-handler.js");
|
|
const handlerCode = `
|
|
export default async function(event) {
|
|
// Test handler
|
|
}
|
|
`;
|
|
await fs.writeFile(handlerPath, handlerCode, "utf-8");
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: path.basename(handlerPath),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(1);
|
|
|
|
const keys = getRegisteredEventKeys();
|
|
expect(keys).toContain("command:new");
|
|
});
|
|
|
|
it("should load multiple handlers", async () => {
|
|
// Create test handler modules
|
|
const handler1Path = path.join(tmpDir, "handler1.js");
|
|
const handler2Path = path.join(tmpDir, "handler2.js");
|
|
|
|
await fs.writeFile(handler1Path, "export default async function() {}", "utf-8");
|
|
await fs.writeFile(handler2Path, "export default async function() {}", "utf-8");
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{ event: "command:new", module: path.basename(handler1Path) },
|
|
{ event: "command:stop", module: path.basename(handler2Path) },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(2);
|
|
|
|
const keys = getRegisteredEventKeys();
|
|
expect(keys).toContain("command:new");
|
|
expect(keys).toContain("command:stop");
|
|
});
|
|
|
|
it("should support named exports", async () => {
|
|
// Create a handler module with named export
|
|
const handlerPath = path.join(tmpDir, "named-export.js");
|
|
const handlerCode = `
|
|
export const myHandler = async function(event) {
|
|
// Named export handler
|
|
}
|
|
`;
|
|
await fs.writeFile(handlerPath, handlerCode, "utf-8");
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: path.basename(handlerPath),
|
|
export: "myHandler",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it("should handle module loading errors gracefully", async () => {
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: "missing-handler.js",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
// Should not throw and should return 0 (handler failed to load)
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it("should handle non-function exports", async () => {
|
|
// Create a module with a non-function export
|
|
const handlerPath = path.join(tmpDir, "bad-export.js");
|
|
await fs.writeFile(handlerPath, 'export default "not a function";', "utf-8");
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: path.basename(handlerPath),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
// Should not throw and should return 0 (handler is not a function)
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it("should handle relative paths", async () => {
|
|
// Create a handler module
|
|
const handlerPath = path.join(tmpDir, "relative-handler.js");
|
|
await fs.writeFile(handlerPath, "export default async function() {}", "utf-8");
|
|
|
|
// Relative to workspaceDir (tmpDir)
|
|
const relativePath = path.relative(tmpDir, handlerPath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: relativePath,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const count = await loadInternalHooks(cfg, tmpDir);
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it("should actually call the loaded handler", async () => {
|
|
// Create a handler that we can verify was called
|
|
const handlerPath = path.join(tmpDir, "callable-handler.js");
|
|
const handlerCode = `
|
|
let callCount = 0;
|
|
export default async function(event) {
|
|
callCount++;
|
|
}
|
|
export function getCallCount() {
|
|
return callCount;
|
|
}
|
|
`;
|
|
await fs.writeFile(handlerPath, handlerCode, "utf-8");
|
|
|
|
const cfg: OpenClawConfig = {
|
|
hooks: {
|
|
internal: {
|
|
enabled: true,
|
|
handlers: [
|
|
{
|
|
event: "command:new",
|
|
module: path.basename(handlerPath),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
await loadInternalHooks(cfg, tmpDir);
|
|
|
|
// Trigger the hook
|
|
const event = createInternalHookEvent("command", "new", "test-session");
|
|
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
|
|
expect(getRegisteredEventKeys()).toContain("command:new");
|
|
});
|
|
});
|
|
});
|