Files
openclaw/extensions/feishu/src/reply-dispatcher.test.ts
2026-02-12 20:19:27 -06:00

117 lines
3.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
const getFeishuRuntimeMock = vi.hoisted(() => vi.fn());
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
const sendMarkdownCardFeishuMock = vi.hoisted(() => vi.fn());
const createFeishuClientMock = vi.hoisted(() => vi.fn());
const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
const createReplyDispatcherWithTypingMock = vi.hoisted(() => vi.fn());
const streamingInstances = vi.hoisted(() => [] as any[]);
vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock }));
vi.mock("./runtime.js", () => ({ getFeishuRuntime: getFeishuRuntimeMock }));
vi.mock("./send.js", () => ({
sendMessageFeishu: sendMessageFeishuMock,
sendMarkdownCardFeishu: sendMarkdownCardFeishuMock,
}));
vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock }));
vi.mock("./targets.js", () => ({ resolveReceiveIdType: resolveReceiveIdTypeMock }));
vi.mock("./streaming-card.js", () => ({
FeishuStreamingSession: class {
active = false;
start = vi.fn(async () => {
this.active = true;
});
update = vi.fn(async () => {});
close = vi.fn(async () => {
this.active = false;
});
isActive = vi.fn(() => this.active);
constructor() {
streamingInstances.push(this);
}
},
}));
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
describe("createFeishuReplyDispatcher streaming behavior", () => {
beforeEach(() => {
vi.clearAllMocks();
streamingInstances.length = 0;
resolveFeishuAccountMock.mockReturnValue({
accountId: "main",
appId: "app_id",
appSecret: "app_secret",
domain: "feishu",
config: {
renderMode: "auto",
streaming: true,
},
});
resolveReceiveIdTypeMock.mockReturnValue("chat_id");
createFeishuClientMock.mockReturnValue({});
createReplyDispatcherWithTypingMock.mockImplementation((opts) => ({
dispatcher: {},
replyOptions: {},
markDispatchIdle: vi.fn(),
_opts: opts,
}));
getFeishuRuntimeMock.mockReturnValue({
channel: {
text: {
resolveTextChunkLimit: vi.fn(() => 4000),
resolveChunkMode: vi.fn(() => "line"),
resolveMarkdownTableMode: vi.fn(() => "preserve"),
convertMarkdownTables: vi.fn((text) => text),
chunkTextWithMode: vi.fn((text) => [text]),
},
reply: {
createReplyDispatcherWithTyping: createReplyDispatcherWithTypingMock,
resolveHumanDelayConfig: vi.fn(() => undefined),
},
},
});
});
it("keeps auto mode plain text on non-streaming send path", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "plain text" }, { kind: "final" });
expect(streamingInstances).toHaveLength(0);
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
it("uses streaming session for auto mode markdown payloads", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
});