diff --git a/extensions/feishu/src/comment-shared.ts b/extensions/feishu/src/comment-shared.ts index 43eb78aeaad..262dff4b7c1 100644 --- a/extensions/feishu/src/comment-shared.ts +++ b/extensions/feishu/src/comment-shared.ts @@ -64,7 +64,7 @@ export function formatFeishuApiError( }); } -export function formatFeishuApiFailure( +function formatFeishuApiFailure( error: unknown, errorPrefix: string, options: { diff --git a/extensions/feishu/src/dedup.ts b/extensions/feishu/src/dedup.ts index ecb53c9159d..f73c0ee7522 100644 --- a/extensions/feishu/src/dedup.ts +++ b/extensions/feishu/src/dedup.ts @@ -118,7 +118,7 @@ export async function tryRecordMessagePersistent( }); } -export async function hasRecordedMessagePersistent( +async function hasRecordedMessagePersistent( messageId: string, namespace = "global", log?: (...args: unknown[]) => void, diff --git a/extensions/feishu/src/directory.test.ts b/extensions/feishu/src/directory.test.ts index 25f5cd61e52..11c266c345b 100644 --- a/extensions/feishu/src/directory.test.ts +++ b/extensions/feishu/src/directory.test.ts @@ -8,15 +8,12 @@ vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock, })); -const { - listFeishuDirectoryGroups, - listFeishuDirectoryGroupsLive, - listFeishuDirectoryPeers, - listFeishuDirectoryPeersLive, -} = await importFreshModule( - import.meta.url, - "./directory.js?directory-test", -); +const { listFeishuDirectoryGroupsLive, listFeishuDirectoryPeersLive } = await importFreshModule< + typeof import("./directory.js") +>(import.meta.url, "./directory.js?directory-test"); +const { listFeishuDirectoryGroups, listFeishuDirectoryPeers } = await importFreshModule< + typeof import("./directory.static.js") +>(import.meta.url, "./directory.static.js?directory-test"); function makeStaticCfg(): ClawdbotConfig { return { diff --git a/extensions/feishu/src/directory.ts b/extensions/feishu/src/directory.ts index 4ff77873f3d..76561c5e953 100644 --- a/extensions/feishu/src/directory.ts +++ b/extensions/feishu/src/directory.ts @@ -9,8 +9,6 @@ import { type FeishuDirectoryPeer, } from "./directory.static.js"; -export { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.static.js"; - export async function listFeishuDirectoryPeersLive(params: { cfg: ClawdbotConfig; query?: string; diff --git a/extensions/feishu/src/monitor.reply-once.lifecycle.test-support.ts b/extensions/feishu/src/monitor.reply-once.lifecycle.test-support.ts deleted file mode 100644 index b07129c42d5..00000000000 --- a/extensions/feishu/src/monitor.reply-once.lifecycle.test-support.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import "./lifecycle.test-support.js"; -import { - getFeishuLifecycleTestMocks, - resetFeishuLifecycleTestMocks, -} from "./lifecycle.test-support.js"; -import { - createFeishuLifecycleConfig, - createFeishuLifecycleReplyDispatcher, - createFeishuTextMessageEvent, - expectFeishuReplyDispatcherSentFinalReplyOnce, - expectFeishuReplyPipelineDedupedAcrossReplay, - expectFeishuReplyPipelineDedupedAfterPostSendFailure, - installFeishuLifecycleReplyRuntime, - mockFeishuReplyOnceDispatch, - restoreFeishuLifecycleStateDir, - setFeishuLifecycleStateDir, - setupFeishuMessageReceiveLifecycleHandler, -} from "./test-support/lifecycle-test-support.js"; - -const { - createFeishuReplyDispatcherMock, - dispatchReplyFromConfigMock, - finalizeInboundContextMock, - resolveAgentRouteMock, - withReplyDispatcherMock, -} = getFeishuLifecycleTestMocks(); - -let lastRuntime = createRuntimeEnv(); -let lifecycleCore: ReturnType; -const handleMessageMock = vi.fn(); -const originalStateDir = process.env.OPENCLAW_STATE_DIR; -const lifecycleConfig = createFeishuLifecycleConfig({ - accountId: "acct-lifecycle", - appId: "cli_test", - appSecret: "secret_test", - accountConfig: { - groupPolicy: "open", - groups: { - oc_group_1: { - requireMention: false, - groupSessionScope: "group_topic_sender", - replyInThread: "enabled", - }, - }, - }, -}); - -async function setupLifecycleMonitor() { - lastRuntime = createRuntimeEnv(); - return setupFeishuMessageReceiveLifecycleHandler({ - runtime: lastRuntime, - core: lifecycleCore, - cfg: lifecycleConfig, - accountId: "acct-lifecycle", - handleMessage: handleMessageMock, - resolveDebounceText: ({ event }) => { - const parsed = JSON.parse(event.message.content) as { text?: string }; - return parsed.text ?? ""; - }, - }); -} - -describe("Feishu reply-once lifecycle", () => { - beforeEach(() => { - vi.useRealTimers(); - resetFeishuLifecycleTestMocks(); - handleMessageMock.mockReset(); - lastRuntime = createRuntimeEnv(); - setFeishuLifecycleStateDir("openclaw-feishu-lifecycle"); - - createFeishuReplyDispatcherMock.mockReturnValue(createFeishuLifecycleReplyDispatcher()); - - resolveAgentRouteMock.mockReturnValue({ - agentId: "main", - channel: "feishu", - accountId: "acct-lifecycle", - sessionKey: "agent:main:feishu:group:oc_group_1", - mainSessionKey: "agent:main:main", - matchedBy: "default", - }); - - mockFeishuReplyOnceDispatch({ - dispatchReplyFromConfigMock, - replyText: "reply once", - }); - - withReplyDispatcherMock.mockImplementation(async ({ run }) => await run()); - handleMessageMock.mockImplementation(async ({ event }) => { - const reply = createFeishuReplyDispatcherMock({ - accountId: "acct-lifecycle", - chatId: event.message.chat_id, - replyToMessageId: event.message.root_id ?? event.message.message_id, - replyInThread: true, - rootId: event.message.root_id, - }); - try { - await withReplyDispatcherMock({ - dispatcher: reply.dispatcher, - onSettled: () => reply.markDispatchIdle(), - run: () => - dispatchReplyFromConfigMock({ - ctx: { - AccountId: "acct-lifecycle", - MessageSid: event.message.message_id, - }, - dispatcher: reply.dispatcher, - }), - }); - } catch (err) { - lastRuntime?.error(`feishu[acct-lifecycle]: failed to dispatch message: ${String(err)}`); - } - }); - - lifecycleCore = installFeishuLifecycleReplyRuntime({ - resolveAgentRouteMock, - finalizeInboundContextMock, - dispatchReplyFromConfigMock, - withReplyDispatcherMock, - storePath: "/tmp/feishu-lifecycle-sessions.json", - }); - }); - - afterEach(() => { - vi.useRealTimers(); - restoreFeishuLifecycleStateDir(originalStateDir); - }); - - it("routes a topic-bound inbound event and emits one reply across duplicate replay", async () => { - const onMessage = await setupLifecycleMonitor(); - const event = createFeishuTextMessageEvent({ - messageId: "om_lifecycle_once", - chatId: "oc_group_1", - rootId: "om_root_topic_1", - threadId: "omt_topic_1", - text: "hello from topic", - }); - - await expectFeishuReplyPipelineDedupedAcrossReplay({ - handler: onMessage, - event, - dispatchReplyFromConfigMock, - createFeishuReplyDispatcherMock, - }); - - expect(lastRuntime?.error).not.toHaveBeenCalled(); - expect(handleMessageMock).toHaveBeenCalledTimes(1); - expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1); - expect(createFeishuReplyDispatcherMock).toHaveBeenCalledTimes(1); - expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith( - expect.objectContaining({ - accountId: "acct-lifecycle", - chatId: "oc_group_1", - replyToMessageId: "om_root_topic_1", - replyInThread: true, - rootId: "om_root_topic_1", - }), - ); - expectFeishuReplyDispatcherSentFinalReplyOnce({ createFeishuReplyDispatcherMock }); - }); - - it("does not duplicate delivery when the first attempt fails after sending the reply", async () => { - const onMessage = await setupLifecycleMonitor(); - const event = createFeishuTextMessageEvent({ - messageId: "om_lifecycle_retry", - chatId: "oc_group_1", - rootId: "om_root_topic_1", - threadId: "omt_topic_1", - text: "hello from topic", - }); - - dispatchReplyFromConfigMock.mockImplementationOnce(async ({ dispatcher }) => { - await dispatcher.sendFinalReply({ text: "reply once" }); - throw new Error("post-send failure"); - }); - - await expectFeishuReplyPipelineDedupedAfterPostSendFailure({ - handler: onMessage, - event, - dispatchReplyFromConfigMock, - runtimeErrorMock: lastRuntime?.error as ReturnType, - }); - - expect(lastRuntime?.error).toHaveBeenCalledTimes(1); - expect(handleMessageMock).toHaveBeenCalledTimes(1); - expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1); - expectFeishuReplyDispatcherSentFinalReplyOnce({ createFeishuReplyDispatcherMock }); - }); -}); diff --git a/extensions/feishu/src/test-support/lifecycle-test-support.ts b/extensions/feishu/src/test-support/lifecycle-test-support.ts index 541f9cbe530..76ff156c414 100644 --- a/extensions/feishu/src/test-support/lifecycle-test-support.ts +++ b/extensions/feishu/src/test-support/lifecycle-test-support.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers"; import { expect, vi, type Mock } from "vitest"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../../runtime-api.js"; -import { createFeishuMessageReceiveHandler } from "../monitor.message-handler.js"; import { setFeishuRuntime } from "../runtime.js"; import type { ResolvedFeishuAccount } from "../types.js"; @@ -411,31 +410,6 @@ async function loadMonitorSingleAccount() { return module.monitorSingleAccount; } -export async function setupFeishuMessageReceiveLifecycleHandler(params: { - runtime: RuntimeEnv; - core: PluginRuntime; - cfg: ClawdbotConfig; - accountId: string; - fireAndForget?: boolean; - handleMessage: Parameters[0]["handleMessage"]; - resolveDebounceText: Parameters< - typeof createFeishuMessageReceiveHandler - >[0]["resolveDebounceText"]; -}): Promise<(data: unknown) => Promise> { - return createFeishuMessageReceiveHandler({ - cfg: params.cfg, - core: params.core, - accountId: params.accountId, - runtime: params.runtime, - chatHistories: new Map(), - fireAndForget: params.fireAndForget, - handleMessage: params.handleMessage, - resolveDebounceText: params.resolveDebounceText, - hasProcessedMessage: vi.fn(async () => false), - recordProcessedMessage: vi.fn(async () => true), - }); -} - export async function setupFeishuLifecycleHandler(params: { createEventDispatcherMock: { mockReturnValue: (value: unknown) => unknown;