Files
clawdbot/src/commands/onboard-hooks.test.ts
2026-01-18 16:28:39 +00:00

246 lines
8.3 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { setupInternalHooks } from "./onboard-hooks.js";
import type { ClawdbotConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { HookStatusReport } from "../hooks/hooks-status.js";
// Mock hook discovery modules
vi.mock("../hooks/hooks-status.js", () => ({
buildWorkspaceHookStatus: vi.fn(),
}));
vi.mock("../agents/agent-scope.js", () => ({
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/mock/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("main"),
}));
describe("onboard-hooks", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockPrompter = (multiselectValue: string[]): WizardPrompter => ({
confirm: vi.fn().mockResolvedValue(true),
note: vi.fn().mockResolvedValue(undefined),
intro: vi.fn().mockResolvedValue(undefined),
outro: vi.fn().mockResolvedValue(undefined),
text: vi.fn().mockResolvedValue(""),
select: vi.fn().mockResolvedValue(""),
multiselect: vi.fn().mockResolvedValue(multiselectValue),
progress: vi.fn().mockReturnValue({
stop: vi.fn(),
update: vi.fn(),
}),
});
const createMockRuntime = (): RuntimeEnv => ({
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
});
const createMockHookReport = (eligible = true): HookStatusReport => ({
workspaceDir: "/mock/workspace",
managedHooksDir: "/mock/.clawdbot/hooks",
hooks: [
{
name: "session-memory",
description: "Save session context to memory when /new command is issued",
source: "clawdbot-bundled",
pluginId: undefined,
filePath: "/mock/workspace/hooks/session-memory/HOOK.md",
baseDir: "/mock/workspace/hooks/session-memory",
handlerPath: "/mock/workspace/hooks/session-memory/handler.js",
hookKey: "session-memory",
emoji: "💾",
events: ["command:new"],
homepage: undefined,
always: false,
disabled: false,
eligible,
managedByPlugin: false,
requirements: {
bins: [],
anyBins: [],
env: [],
config: ["workspace.dir"],
os: [],
},
missing: {
bins: [],
anyBins: [],
env: [],
config: eligible ? [] : ["workspace.dir"],
os: [],
},
configChecks: [],
install: [],
},
{
name: "command-logger",
description: "Log all command events to a centralized audit file",
source: "clawdbot-bundled",
pluginId: undefined,
filePath: "/mock/workspace/hooks/command-logger/HOOK.md",
baseDir: "/mock/workspace/hooks/command-logger",
handlerPath: "/mock/workspace/hooks/command-logger/handler.js",
hookKey: "command-logger",
emoji: "📝",
events: ["command"],
homepage: undefined,
always: false,
disabled: false,
eligible,
managedByPlugin: false,
requirements: {
bins: [],
anyBins: [],
env: [],
config: ["workspace.dir"],
os: [],
},
missing: {
bins: [],
anyBins: [],
env: [],
config: eligible ? [] : ["workspace.dir"],
os: [],
},
configChecks: [],
install: [],
},
],
});
describe("setupInternalHooks", () => {
it("should enable hooks when user selects them", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
const cfg: ClawdbotConfig = {};
const prompter = createMockPrompter(["session-memory"]);
const runtime = createMockRuntime();
const result = await setupInternalHooks(cfg, runtime, prompter);
expect(result.hooks?.internal?.enabled).toBe(true);
expect(result.hooks?.internal?.entries).toEqual({
"session-memory": { enabled: true },
});
expect(prompter.note).toHaveBeenCalledTimes(2);
expect(prompter.multiselect).toHaveBeenCalledWith({
message: "Enable hooks?",
options: [
{ value: "__skip__", label: "Skip for now" },
{
value: "session-memory",
label: "💾 session-memory",
hint: "Save session context to memory when /new command is issued",
},
{
value: "command-logger",
label: "📝 command-logger",
hint: "Log all command events to a centralized audit file",
},
],
});
});
it("should not enable hooks when user skips", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
const cfg: ClawdbotConfig = {};
const prompter = createMockPrompter(["__skip__"]);
const runtime = createMockRuntime();
const result = await setupInternalHooks(cfg, runtime, prompter);
expect(result.hooks?.internal).toBeUndefined();
expect(prompter.note).toHaveBeenCalledTimes(1);
});
it("should handle no eligible hooks", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport(false));
const cfg: ClawdbotConfig = {};
const prompter = createMockPrompter([]);
const runtime = createMockRuntime();
const result = await setupInternalHooks(cfg, runtime, prompter);
expect(result).toEqual(cfg);
expect(prompter.multiselect).not.toHaveBeenCalled();
expect(prompter.note).toHaveBeenCalledWith(
"No eligible hooks found. You can configure hooks later in your config.",
"No Hooks Available",
);
});
it("should preserve existing hooks config when enabled", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
const cfg: ClawdbotConfig = {
hooks: {
enabled: true,
path: "/webhook",
token: "existing-token",
},
};
const prompter = createMockPrompter(["session-memory"]);
const runtime = createMockRuntime();
const result = await setupInternalHooks(cfg, runtime, prompter);
expect(result.hooks?.enabled).toBe(true);
expect(result.hooks?.path).toBe("/webhook");
expect(result.hooks?.token).toBe("existing-token");
expect(result.hooks?.internal?.enabled).toBe(true);
expect(result.hooks?.internal?.entries).toEqual({
"session-memory": { enabled: true },
});
});
it("should preserve existing config when user skips", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
const cfg: ClawdbotConfig = {
agents: { defaults: { workspace: "/workspace" } },
};
const prompter = createMockPrompter(["__skip__"]);
const runtime = createMockRuntime();
const result = await setupInternalHooks(cfg, runtime, prompter);
expect(result).toEqual(cfg);
expect(result.agents?.defaults?.workspace).toBe("/workspace");
});
it("should show informative notes to user", async () => {
const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js");
vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport());
const cfg: ClawdbotConfig = {};
const prompter = createMockPrompter(["session-memory"]);
const runtime = createMockRuntime();
await setupInternalHooks(cfg, runtime, prompter);
const noteCalls = (prompter.note as ReturnType<typeof vi.fn>).mock.calls;
expect(noteCalls).toHaveLength(2);
// First note should explain what hooks are
expect(noteCalls[0][0]).toContain("Hooks let you automate actions");
expect(noteCalls[0][0]).toContain("automate actions");
// Second note should confirm configuration
expect(noteCalls[1][0]).toContain("Enabled 1 hook: session-memory");
expect(noteCalls[1][0]).toContain("clawdbot hooks list");
});
});
});