fix(hooks): report eventless hooks as not ready (#116083)

Fixes #72370

Release note: hooks check now reports selected hooks without declared events as not ready instead of ready.
This commit is contained in:
Vincent Koc
2026-07-30 05:29:25 +08:00
committed by GitHub
parent d98e984ec2
commit 215e49b1a2
3 changed files with 94 additions and 3 deletions
+41
View File
@@ -85,6 +85,20 @@ function createPluginManagedHookReport(): HookStatusReport {
};
}
function createEventlessHookReport(): HookStatusReport {
return {
...report,
hooks: [
{
...expectDefined(report.hooks[0], "report.hooks[0] test invariant"),
events: [],
loadable: false,
blockedReason: "no events defined",
},
],
};
}
describe("hooks cli formatting", () => {
it("labels hooks list output", () => {
const output = formatHooksList(report, {});
@@ -97,6 +111,33 @@ describe("hooks cli formatting", () => {
expect(output).toContain("Hooks Status");
});
it("classifies eventless hooks as not ready in human check output", () => {
const output = formatHooksCheck(createEventlessHookReport(), {});
expect(output).toContain("Ready: 0");
expect(output).toContain("Not ready: 1");
expect(output).toContain("session-memory - no events defined");
});
it("classifies eventless hooks as not eligible in JSON check output", () => {
const output = JSON.parse(formatHooksCheck(createEventlessHookReport(), { json: true }));
expect(output).toMatchObject({
total: 1,
eligible: 0,
notEligible: 1,
hooks: {
eligible: [],
notEligible: [
{
name: "session-memory",
blockedReason: "no events defined",
},
],
},
});
});
it("labels plugin-managed hooks with plugin id", () => {
const pluginReport = createPluginManagedHookReport();
+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { buildWorkspaceHookStatus } from "./hooks-status.js";
import type { HookEntry } from "./types.js";
function createHookEntry(params: {
source: HookEntry["hook"]["source"];
events: string[];
}): HookEntry {
return {
hook: {
name: "session-memory",
description: "Save session context to memory",
source: params.source,
filePath: `/tmp/${params.source}/HOOK.md`,
baseDir: `/tmp/${params.source}`,
handlerPath: `/tmp/${params.source}/handler.js`,
},
frontmatter: {},
metadata: { events: params.events },
};
}
describe("hook status", () => {
it("reports an eventless managed winner as not loadable", () => {
const report = buildWorkspaceHookStatus("/tmp/workspace", {
entries: [
createHookEntry({
source: "openclaw-managed",
events: [],
}),
createHookEntry({
source: "openclaw-workspace",
events: ["command:new"],
}),
],
});
expect(report.hooks).toHaveLength(1);
expect(report.hooks[0]).toMatchObject({
source: "openclaw-managed",
events: [],
enabledByConfig: true,
requirementsSatisfied: true,
loadable: false,
blockedReason: "no events defined",
});
});
});
+5 -3
View File
@@ -42,7 +42,7 @@ export type HookStatusEntry = {
enabledByConfig: boolean;
requirementsSatisfied: boolean;
loadable: boolean;
blockedReason?: HookEnableStateReason | "missing requirements";
blockedReason?: HookEnableStateReason | "missing requirements" | "no events defined";
managedByPlugin: boolean;
requirements: Requirements;
missing: Requirements;
@@ -114,9 +114,11 @@ function buildHookStatus(
});
const enabledByConfig = enableState.enabled;
const loadable = enabledByConfig && requirementsSatisfied;
const hasEvents = events.length > 0;
const loadable = enabledByConfig && requirementsSatisfied && hasEvents;
const blockedReason =
enableState.reason ?? (requirementsSatisfied ? undefined : "missing requirements");
enableState.reason ??
(!requirementsSatisfied ? "missing requirements" : hasEvents ? undefined : "no events defined");
return {
name: entry.hook.name,