refactor: trim feishu helper exports

This commit is contained in:
Peter Steinberger
2026-05-02 09:09:35 +01:00
parent b8ddb8a494
commit 9880b7c914
6 changed files with 8 additions and 229 deletions

View File

@@ -64,7 +64,7 @@ export function formatFeishuApiError(
});
}
export function formatFeishuApiFailure(
function formatFeishuApiFailure(
error: unknown,
errorPrefix: string,
options: {

View File

@@ -118,7 +118,7 @@ export async function tryRecordMessagePersistent(
});
}
export async function hasRecordedMessagePersistent(
async function hasRecordedMessagePersistent(
messageId: string,
namespace = "global",
log?: (...args: unknown[]) => void,

View File

@@ -8,15 +8,12 @@ vi.mock("./client.js", () => ({
createFeishuClient: createFeishuClientMock,
}));
const {
listFeishuDirectoryGroups,
listFeishuDirectoryGroupsLive,
listFeishuDirectoryPeers,
listFeishuDirectoryPeersLive,
} = await importFreshModule<typeof import("./directory.js")>(
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 {

View File

@@ -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;

View File

@@ -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<typeof installFeishuLifecycleReplyRuntime>;
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<typeof vi.fn>,
});
expect(lastRuntime?.error).toHaveBeenCalledTimes(1);
expect(handleMessageMock).toHaveBeenCalledTimes(1);
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
expectFeishuReplyDispatcherSentFinalReplyOnce({ createFeishuReplyDispatcherMock });
});
});

View File

@@ -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<typeof createFeishuMessageReceiveHandler>[0]["handleMessage"];
resolveDebounceText: Parameters<
typeof createFeishuMessageReceiveHandler
>[0]["resolveDebounceText"];
}): Promise<(data: unknown) => Promise<void>> {
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;