melbot / src /hooks /loader.test.ts
amos-fernandes's picture
Upload 4501 files
3a65265 verified
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadInternalHooks } from "./loader.js";
import {
clearInternalHooks,
getRegisteredEventKeys,
triggerInternalHook,
createInternalHookEvent,
} from "./internal-hooks.js";
import type { MoltbotConfig } from "../config/config.js";
describe("loader", () => {
let tmpDir: string;
let originalBundledDir: string | undefined;
beforeEach(async () => {
clearInternalHooks();
// Create a temp directory for test modules
tmpDir = path.join(os.tmpdir(), `moltbot-test-${Date.now()}`);
await fs.mkdir(tmpDir, { recursive: true });
// Disable bundled hooks during tests by setting env var to non-existent directory
originalBundledDir = process.env.CLAWDBOT_BUNDLED_HOOKS_DIR;
process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks";
});
afterEach(async () => {
clearInternalHooks();
// Restore original env var
if (originalBundledDir === undefined) {
delete process.env.CLAWDBOT_BUNDLED_HOOKS_DIR;
} else {
process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = originalBundledDir;
}
// Clean up temp directory
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe("loadInternalHooks", () => {
it("should return 0 when hooks are not enabled", async () => {
const cfg: MoltbotConfig = {
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: MoltbotConfig = {};
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: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: 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: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{ event: "command:new", module: handler1Path },
{ event: "command:stop", module: 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: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: handlerPath,
export: "myHandler",
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
});
it("should handle module loading errors gracefully", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
const cfg: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: "/nonexistent/path/handler.js",
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining("Failed to load hook handler"),
expect.any(String),
);
consoleError.mockRestore();
});
it("should handle non-function exports", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
// 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: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: handlerPath,
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining("is not a function"));
consoleError.mockRestore();
});
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");
// Get relative path from cwd
const relativePath = path.relative(process.cwd(), handlerPath);
const cfg: MoltbotConfig = {
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: MoltbotConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: 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");
});
});
});