Files
openclaw/src/hooks/plugin-hooks.test.ts
2026-03-15 16:08:50 -07:00

159 lines
4.9 KiB
TypeScript

import fs from "node:fs";
import fsp 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,
createInternalHookEvent,
triggerInternalHook,
} from "./internal-hooks.js";
import { loadInternalHooks } from "./loader.js";
import { loadWorkspaceHookEntries } from "./workspace.js";
describe("bundle plugin hooks", () => {
let fixtureRoot = "";
let caseId = 0;
let workspaceDir = "";
let previousBundledHooksDir: string | undefined;
beforeAll(async () => {
fixtureRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-hooks-"));
});
beforeEach(async () => {
clearInternalHooks();
workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
await fsp.mkdir(workspaceDir, { recursive: true });
previousBundledHooksDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks";
});
afterEach(() => {
clearInternalHooks();
if (previousBundledHooksDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = previousBundledHooksDir;
}
});
afterAll(async () => {
await fsp.rm(fixtureRoot, { recursive: true, force: true });
});
async function writeBundleHookFixture(): Promise<string> {
const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", "sample-bundle");
const hookDir = path.join(bundleRoot, "hooks", "bundle-hook");
await fsp.mkdir(path.join(bundleRoot, ".codex-plugin"), { recursive: true });
await fsp.mkdir(hookDir, { recursive: true });
await fsp.writeFile(
path.join(bundleRoot, ".codex-plugin", "plugin.json"),
JSON.stringify({
name: "Sample Bundle",
hooks: "hooks",
}),
"utf-8",
);
await fsp.writeFile(
path.join(hookDir, "HOOK.md"),
[
"---",
"name: bundle-hook",
'description: "Bundle hook"',
'metadata: {"openclaw":{"events":["command:new"]}}',
"---",
"",
"# Bundle hook",
"",
].join("\n"),
"utf-8",
);
await fsp.writeFile(
path.join(hookDir, "handler.js"),
'export default async function(event) { event.messages.push("bundle-hook-ok"); }\n',
"utf-8",
);
return bundleRoot;
}
function createConfig(enabled: boolean): OpenClawConfig {
return {
hooks: {
internal: {
enabled: true,
},
},
plugins: {
entries: {
"sample-bundle": {
enabled,
},
},
},
};
}
it("exposes enabled bundle hook dirs as plugin-managed hook entries", async () => {
const bundleRoot = await writeBundleHookFixture();
const entries = loadWorkspaceHookEntries(workspaceDir, {
config: createConfig(true),
});
expect(entries).toHaveLength(1);
expect(entries[0]?.hook.name).toBe("bundle-hook");
expect(entries[0]?.hook.source).toBe("openclaw-plugin");
expect(entries[0]?.hook.pluginId).toBe("sample-bundle");
expect(entries[0]?.hook.baseDir).toBe(
fs.realpathSync.native(path.join(bundleRoot, "hooks", "bundle-hook")),
);
expect(entries[0]?.metadata?.events).toEqual(["command:new"]);
});
it("loads and executes enabled bundle hooks through the internal hook loader", async () => {
await writeBundleHookFixture();
const count = await loadInternalHooks(createConfig(true), workspaceDir);
expect(count).toBe(1);
const event = createInternalHookEvent("command", "new", "test-session");
await triggerInternalHook(event);
expect(event.messages).toContain("bundle-hook-ok");
});
it("skips disabled bundle hooks", async () => {
await writeBundleHookFixture();
const entries = loadWorkspaceHookEntries(workspaceDir, {
config: createConfig(false),
});
expect(entries).toHaveLength(0);
});
it("does not treat Claude hooks.json bundles as OpenClaw hook packs", async () => {
const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", "claude-bundle");
await fsp.mkdir(path.join(bundleRoot, ".claude-plugin"), { recursive: true });
await fsp.mkdir(path.join(bundleRoot, "hooks"), { recursive: true });
await fsp.writeFile(
path.join(bundleRoot, ".claude-plugin", "plugin.json"),
JSON.stringify({
name: "Claude Bundle",
hooks: [{ type: "command" }],
}),
"utf-8",
);
await fsp.writeFile(path.join(bundleRoot, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8");
const entries = loadWorkspaceHookEntries(workspaceDir, {
config: {
hooks: { internal: { enabled: true } },
plugins: { entries: { "claude-bundle": { enabled: true } } },
},
});
expect(entries).toHaveLength(0);
});
});