import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerFeishuSubagentHooks } from "./subagent-hooks.js"; import { __testing as threadBindingTesting, createFeishuThreadBindingManager, } from "./thread-bindings.js"; const baseConfig = { session: { mainKey: "main", scope: "per-sender" }, channels: { feishu: {} }, }; function registerHandlersForTest(config: Record = baseConfig) { const handlers = new Map unknown>(); const api = { 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; } describe("feishu subagent hook handlers", () => { beforeEach(() => { threadBindingTesting.resetFeishuThreadBindingsForTests(); }); it("registers Feishu subagent hooks", () => { const handlers = registerHandlersForTest(); expect(handlers.has("subagent_spawning")).toBe(true); expect(handlers.has("subagent_delivery_target")).toBe(true); expect(handlers.has("subagent_ended")).toBe(true); expect(handlers.has("subagent_spawned")).toBe(false); }); it("binds a Feishu DM conversation on subagent_spawning", async () => { const handlers = registerHandlersForTest(); const handler = getRequiredHandler(handlers, "subagent_spawning"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); const result = await handler( { childSessionKey: "agent:main:subagent:child", agentId: "codex", label: "banana", mode: "session", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, threadRequested: true, }, {}, ); expect(result).toEqual({ status: "ok", threadBindingReady: true }); const deliveryTargetHandler = getRequiredHandler(handlers, "subagent_delivery_target"); expect( deliveryTargetHandler( { childSessionKey: "agent:main:subagent:child", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, expectsCompletionMessage: true, }, {}, ), ).toEqual({ origin: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, }); }); it("preserves the original Feishu DM delivery target", async () => { const handlers = registerHandlersForTest(); const deliveryHandler = getRequiredHandler(handlers, "subagent_delivery_target"); const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ conversationId: "ou_sender_1", targetKind: "subagent", targetSessionKey: "agent:main:subagent:chat-dm-child", metadata: { deliveryTo: "chat:oc_dm_chat_1", boundBy: "system", }, }); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:chat-dm-child", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "chat:oc_dm_chat_1", }, expectsCompletionMessage: true, }, {}, ), ).toEqual({ origin: { channel: "feishu", accountId: "work", to: "chat:oc_dm_chat_1", }, }); }); 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"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); const result = await spawnHandler( { childSessionKey: "agent:main:subagent:topic-child", agentId: "codex", label: "topic-child", mode: "session", requester: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, threadRequested: true, }, {}, ); expect(result).toEqual({ status: "ok", threadBindingReady: true }); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:topic-child", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, expectsCompletionMessage: true, }, {}, ), ).toEqual({ origin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, }); }); 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 manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", parentConversationId: "oc_group_chat", targetKind: "subagent", targetSessionKey: "agent:main:parent", metadata: { agentId: "codex", label: "parent", boundBy: "system", }, }); const reboundResult = await spawnHandler( { childSessionKey: "agent:main:subagent:sender-child", agentId: "codex", label: "sender-child", mode: "session", requester: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, threadRequested: true, }, { requesterSessionKey: "agent:main:parent", }, ); expect(reboundResult).toEqual({ status: "ok", threadBindingReady: true }); expect(manager.listBySessionKey("agent:main:subagent:sender-child")).toMatchObject([ { conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", parentConversationId: "oc_group_chat", }, ]); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:sender-child", requesterSessionKey: "agent:main:parent", requesterOrigin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, expectsCompletionMessage: true, }, {}, ), ).toEqual({ origin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, }); }); 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"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await spawnHandler( { childSessionKey: "agent:main:subagent:shared", agentId: "codex", label: "shared", mode: "session", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, threadRequested: true, }, {}, ); await spawnHandler( { childSessionKey: "agent:main:subagent:shared", agentId: "codex", label: "shared", mode: "session", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_2", }, threadRequested: true, }, {}, ); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:shared", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "user:ou_sender_2", }, expectsCompletionMessage: true, }, {}, ), ).toEqual({ origin: { channel: "feishu", accountId: "work", to: "user:ou_sender_2", }, }); }); 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 manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", parentConversationId: "oc_group_chat", targetKind: "subagent", targetSessionKey: "agent:main:parent", metadata: { boundBy: "system" }, }); manager.bindConversation({ conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_2", parentConversationId: "oc_group_chat", targetKind: "subagent", targetSessionKey: "agent:main:parent", metadata: { boundBy: "system" }, }); await expect( spawnHandler( { childSessionKey: "agent:main:subagent:ambiguous-child", agentId: "codex", label: "ambiguous-child", mode: "session", requester: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, threadRequested: true, }, { requesterSessionKey: "agent:main:parent", }, ), ).resolves.toMatchObject({ status: "error", error: expect.stringContaining("direct messages or topic conversations"), }); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:ambiguous-child", requesterSessionKey: "agent:main:parent", requesterOrigin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, expectsCompletionMessage: true, }, {}, ), ).toBeUndefined(); }); 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 manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); manager.bindConversation({ conversationId: "oc_group_chat:topic:om_topic_root", parentConversationId: "oc_group_chat", targetKind: "subagent", targetSessionKey: "agent:main:parent", metadata: { boundBy: "system" }, }); manager.bindConversation({ conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", parentConversationId: "oc_group_chat", targetKind: "subagent", targetSessionKey: "agent:main:parent", metadata: { boundBy: "system" }, }); await expect( spawnHandler( { childSessionKey: "agent:main:subagent:mixed-topic-child", agentId: "codex", label: "mixed-topic-child", mode: "session", requester: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, threadRequested: true, }, { requesterSessionKey: "agent:main:parent", }, ), ).resolves.toMatchObject({ status: "error", error: expect.stringContaining("direct messages or topic conversations"), }); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:mixed-topic-child", requesterSessionKey: "agent:main:parent", requesterOrigin: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", threadId: "om_topic_root", }, expectsCompletionMessage: true, }, {}, ), ).toBeUndefined(); }); 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"); await expect( spawnHandler( { childSessionKey: "agent:main:subagent:child", agentId: "codex", mode: "run", requester: { channel: "discord", accountId: "work", to: "channel:123", }, threadRequested: true, }, {}, ), ).resolves.toBeUndefined(); await expect( spawnHandler( { childSessionKey: "agent:main:subagent:child", agentId: "codex", mode: "run", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, threadRequested: false, }, {}, ), ).resolves.toBeUndefined(); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:child", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "discord", accountId: "work", to: "channel:123", }, expectsCompletionMessage: true, }, {}, ), ).toBeUndefined(); expect( endedHandler( { targetSessionKey: "agent:main:subagent:child", targetKind: "subagent", reason: "done", accountId: "work", }, {}, ), ).toBeUndefined(); }); it("returns an error for unsupported non-topic Feishu group conversations", async () => { const handler = getRequiredHandler(registerHandlersForTest(), "subagent_spawning"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await expect( handler( { childSessionKey: "agent:main:subagent:child", agentId: "codex", mode: "session", requester: { channel: "feishu", accountId: "work", to: "chat:oc_group_chat", }, threadRequested: true, }, {}, ), ).resolves.toMatchObject({ status: "error", error: expect.stringContaining("direct messages or topic conversations"), }); }); 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"); createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" }); await spawnHandler( { childSessionKey: "agent:main:subagent:child", agentId: "codex", mode: "session", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, threadRequested: true, }, {}, ); endedHandler( { targetSessionKey: "agent:main:subagent:child", targetKind: "subagent", reason: "done", accountId: "work", }, {}, ); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:child", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, expectsCompletionMessage: true, }, {}, ), ).toBeUndefined(); }); 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"); await expect( spawnHandler( { childSessionKey: "agent:main:subagent:no-manager", agentId: "codex", mode: "session", requester: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, threadRequested: true, }, {}, ), ).resolves.toMatchObject({ status: "error", error: expect.stringContaining("monitor is not active"), }); expect( deliveryHandler( { childSessionKey: "agent:main:subagent:no-manager", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "feishu", accountId: "work", to: "user:ou_sender_1", }, expectsCompletionMessage: true, }, {}, ), ).toBeUndefined(); }); });