Spaces:
Configuration error
Configuration error
| 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"); | |
| }); | |
| }); | |
| }); | |