diff --git a/extensions/discord/src/subagent-hooks.test.ts b/extensions/discord/src/subagent-hooks.test.ts index 9ba082144e6..6d22ea1ff54 100644 --- a/extensions/discord/src/subagent-hooks.test.ts +++ b/extensions/discord/src/subagent-hooks.test.ts @@ -1,5 +1,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + getRequiredHookHandler, + registerHookHandlersForTest, +} from "../../test-utils/subagent-hooks.js"; import { registerDiscordSubagentHooks } from "./subagent-hooks.js"; type ThreadBindingRecord = { @@ -55,26 +59,10 @@ function registerHandlersForTest( }, }, ) { - const handlers = new Map unknown>(); - const api = { + return registerHookHandlersForTest({ config, - on: (hookName: string, handler: (event: unknown, ctx: unknown) => unknown) => { - handlers.set(hookName, handler); - }, - } as unknown as OpenClawPluginApi; - registerDiscordSubagentHooks(api); - return handlers; -} - -function getRequiredHandler( - handlers: Map unknown>, - hookName: string, -): (event: unknown, ctx: unknown) => unknown { - const handler = handlers.get(hookName); - if (!handler) { - throw new Error(`expected ${hookName} hook handler`); - } - return handler; + register: registerDiscordSubagentHooks, + }); } function resolveSubagentDeliveryTargetForTest(requesterOrigin: { @@ -84,7 +72,7 @@ function resolveSubagentDeliveryTargetForTest(requesterOrigin: { threadId?: string; }) { const handlers = registerHandlersForTest(); - const handler = getRequiredHandler(handlers, "subagent_delivery_target"); + const handler = getRequiredHookHandler(handlers, "subagent_delivery_target"); return handler( { childSessionKey: "agent:main:subagent:child", @@ -158,7 +146,7 @@ async function runSubagentSpawning( event = createSpawnEventWithoutThread(), ) { const handlers = registerHandlersForTest(config); - const handler = getRequiredHandler(handlers, "subagent_spawning"); + const handler = getRequiredHookHandler(handlers, "subagent_spawning"); return await handler(event, {}); } @@ -202,7 +190,7 @@ describe("discord subagent hook handlers", () => { it("binds thread routing on subagent_spawning", async () => { const handlers = registerHandlersForTest(); - const handler = getRequiredHandler(handlers, "subagent_spawning"); + const handler = getRequiredHookHandler(handlers, "subagent_spawning"); const result = await handler(createSpawnEvent(), {}); @@ -320,7 +308,7 @@ describe("discord subagent hook handlers", () => { it("unbinds thread routing on subagent_ended", () => { const handlers = registerHandlersForTest(); - const handler = getRequiredHandler(handlers, "subagent_ended"); + const handler = getRequiredHookHandler(handlers, "subagent_ended"); handler( { diff --git a/extensions/feishu/src/subagent-hooks.test.ts b/extensions/feishu/src/subagent-hooks.test.ts index a86e8996f35..df2c276ad95 100644 --- a/extensions/feishu/src/subagent-hooks.test.ts +++ b/extensions/feishu/src/subagent-hooks.test.ts @@ -1,5 +1,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + getRequiredHookHandler, + registerHookHandlersForTest, +} from "../../test-utils/subagent-hooks.js"; import { registerFeishuSubagentHooks } from "./subagent-hooks.js"; import { __testing as threadBindingTesting, @@ -12,26 +16,10 @@ const baseConfig = { }; function registerHandlersForTest(config: Record = baseConfig) { - const handlers = new Map unknown>(); - const api = { + return registerHookHandlersForTest({ config, - on: (hookName: string, handler: (event: unknown, ctx: unknown) => unknown) => { - handlers.set(hookName, handler); - }, - } as unknown as OpenClawPluginApi; - registerFeishuSubagentHooks(api); - return handlers; -} - -function getRequiredHandler( - handlers: Map unknown>, - hookName: string, -): (event: unknown, ctx: unknown) => unknown { - const handler = handlers.get(hookName); - if (!handler) { - throw new Error(`expected ${hookName} hook handler`); - } - return handler; + register: registerFeishuSubagentHooks, + }); } describe("feishu subagent hook handlers", () => { @@ -49,7 +37,7 @@ describe("feishu subagent hook handlers", () => { it("binds a Feishu DM conversation on subagent_spawning", async () => { const handlers = registerHandlersForTest(); - const handler = getRequiredHandler(handlers, "subagent_spawning"); + const handler = getRequiredHookHandler(handlers, "subagent_spawning"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); const result = await handler( @@ -70,7 +58,7 @@ describe("feishu subagent hook handlers", () => { expect(result).toEqual({ status: "ok", threadBindingReady: true }); - const deliveryTargetHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const deliveryTargetHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); expect( deliveryTargetHandler( { @@ -96,7 +84,7 @@ describe("feishu subagent hook handlers", () => { it("preserves the original Feishu DM delivery target", async () => { const handlers = registerHandlersForTest(); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ @@ -134,8 +122,8 @@ describe("feishu subagent hook handlers", () => { it("binds a Feishu topic conversation and preserves parent context", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); const result = await spawnHandler( @@ -183,8 +171,8 @@ describe("feishu subagent hook handlers", () => { it("uses the requester session binding to preserve sender-scoped topic conversations", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ @@ -252,8 +240,8 @@ describe("feishu subagent hook handlers", () => { it("prefers requester-matching bindings when multiple child bindings exist", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await spawnHandler( @@ -312,8 +300,8 @@ describe("feishu subagent hook handlers", () => { it("fails closed when requester-session bindings remain ambiguous for the same topic", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ @@ -375,8 +363,8 @@ describe("feishu subagent hook handlers", () => { it("fails closed when both topic-level and sender-scoped requester bindings exist", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ @@ -438,9 +426,9 @@ describe("feishu subagent hook handlers", () => { it("no-ops for non-Feishu channels and non-threaded spawns", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); - const endedHandler = getRequiredHandler(handlers, "subagent_ended"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); + const endedHandler = getRequiredHookHandler(handlers, "subagent_ended"); await expect( spawnHandler( @@ -506,7 +494,7 @@ describe("feishu subagent hook handlers", () => { }); it("returns an error for unsupported non-topic Feishu group conversations", async () => { - const handler = getRequiredHandler(registerHandlersForTest(), "subagent_spawning"); + const handler = getRequiredHookHandler(registerHandlersForTest(), "subagent_spawning"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await expect( @@ -532,9 +520,9 @@ describe("feishu subagent hook handlers", () => { it("unbinds Feishu bindings on subagent_ended", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); - const endedHandler = getRequiredHandler(handlers, "subagent_ended"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); + const endedHandler = getRequiredHookHandler(handlers, "subagent_ended"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await spawnHandler( @@ -581,8 +569,8 @@ describe("feishu subagent hook handlers", () => { it("fails closed when the Feishu monitor-owned binding manager is unavailable", async () => { const handlers = registerHandlersForTest(); - const spawnHandler = getRequiredHandler(handlers, "subagent_spawning"); - const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); + const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning"); + const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target"); await expect( spawnHandler( diff --git a/extensions/test-utils/subagent-hooks.ts b/extensions/test-utils/subagent-hooks.ts new file mode 100644 index 00000000000..2cd80fc5a35 --- /dev/null +++ b/extensions/test-utils/subagent-hooks.ts @@ -0,0 +1,25 @@ +export function registerHookHandlersForTest(params: { + config: Record; + register: (api: TApi) => void; +}) { + const handlers = new Map unknown>(); + const api = { + config: params.config, + on: (hookName: string, handler: (event: unknown, ctx: unknown) => unknown) => { + handlers.set(hookName, handler); + }, + } as TApi; + params.register(api); + return handlers; +} + +export function getRequiredHookHandler( + handlers: Map unknown>, + hookName: string, +): (event: unknown, ctx: unknown) => unknown { + const handler = handlers.get(hookName); + if (!handler) { + throw new Error(`expected ${hookName} hook handler`); + } + return handler; +}