From 9a143073065f10b895e47464df4872523db457fd Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 12:22:21 -0700 Subject: [PATCH] test(plugins): pin bundled hook names --- .../contracts/boundary-invariants.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/plugins/contracts/boundary-invariants.test.ts b/src/plugins/contracts/boundary-invariants.test.ts index 9453e0ab7b5..cbc1eb9d4ac 100644 --- a/src/plugins/contracts/boundary-invariants.test.ts +++ b/src/plugins/contracts/boundary-invariants.test.ts @@ -19,6 +19,33 @@ const BUNDLED_TYPED_HOOK_REGISTRATION_FILES = [ "extensions/skill-workshop/index.ts", "extensions/thread-ownership/index.ts", ] as const; +const BUNDLED_TYPED_HOOK_REGISTRATION_GUARDS = { + "extensions/acpx/index.ts": ["reply_dispatch"], + "extensions/active-memory/index.ts": ["before_prompt_build"], + "extensions/diffs/src/plugin.ts": ["before_prompt_build"], + "extensions/discord/subagent-hooks-api.ts": [ + "subagent_delivery_target", + "subagent_ended", + "subagent_spawning", + ], + "extensions/feishu/subagent-hooks-api.ts": [ + "subagent_delivery_target", + "subagent_ended", + "subagent_spawning", + ], + "extensions/matrix/subagent-hooks-api.ts": [ + "subagent_delivery_target", + "subagent_ended", + "subagent_spawning", + ], + "extensions/memory-core/src/dreaming.ts": ["before_agent_reply", "gateway_start"], + "extensions/memory-lancedb/index.ts": ["agent_end", "before_prompt_build"], + "extensions/skill-workshop/index.ts": ["agent_end", "before_prompt_build"], + "extensions/thread-ownership/index.ts": ["message_received", "message_sending"], +} as const satisfies Record< + (typeof BUNDLED_TYPED_HOOK_REGISTRATION_FILES)[number], + readonly string[] +>; type FileFilter = { excludeTests?: boolean; @@ -86,6 +113,13 @@ function collectBundledExtensionImports(source: string): string[] { .filter((specifier): specifier is string => typeof specifier === "string"); } +function collectTypedHookNames(source: string): string[] { + return [...source.matchAll(/\bapi\.on\(\s*"([^"]+)"/gu)] + .map((match) => match[1]) + .filter((hookName): hookName is string => typeof hookName === "string") + .toSorted(); +} + describe("plugin contract boundary invariants", () => { it("keeps bundled-capability-metadata confined to contract/test inventory", () => { const files = listTsFiles("src"); @@ -163,6 +197,17 @@ describe("plugin contract boundary invariants", () => { expect(hookRegistrationFiles).toEqual(BUNDLED_TYPED_HOOK_REGISTRATION_FILES); }); + it("keeps bundled plugin typed hook names on an explicit allowlist", () => { + expect( + Object.fromEntries( + BUNDLED_TYPED_HOOK_REGISTRATION_FILES.map((file) => [ + file, + collectTypedHookNames(readRepoSource(file)), + ]), + ), + ).toEqual(BUNDLED_TYPED_HOOK_REGISTRATION_GUARDS); + }); + it("keeps bundled plugin production code off raw registerHook calls", () => { const files = listTsFiles("extensions", { excludeTests: true }); const offenders = files.filter((file) => /\bregisterHook\(/u.test(readRepoSource(file)));