test: reduce feishu reply dispatcher duplication

This commit is contained in:
Peter Steinberger
2026-03-13 20:46:27 +00:00
parent d347a4426d
commit 3ffb9f19cb

View File

@@ -63,6 +63,8 @@ vi.mock("./streaming-card.js", () => ({
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
describe("createFeishuReplyDispatcher streaming behavior", () => {
type ReplyDispatcherArgs = Parameters<typeof createFeishuReplyDispatcher>[0];
beforeEach(() => {
vi.clearAllMocks();
streamingInstances.length = 0;
@@ -128,6 +130,25 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
return createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
}
function createRuntimeLogger() {
return { log: vi.fn(), error: vi.fn() } as never;
}
function createDispatcherHarness(overrides: Partial<ReplyDispatcherArgs> = {}) {
const result = createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
...overrides,
});
return {
result,
options: createReplyDispatcherWithTypingMock.mock.calls.at(-1)?.[0],
};
}
it("skips typing indicator when account typingIndicator is disabled", async () => {
resolveFeishuAccountMock.mockReturnValue({
accountId: "main",
@@ -209,14 +230,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
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];
const { options } = createDispatcherHarness();
await options.deliver({ text: "plain text" }, { kind: "final" });
expect(streamingInstances).toHaveLength(0);
@@ -225,14 +239,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("suppresses internal block payload delivery", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
const { options } = createDispatcherHarness();
await options.deliver({ text: "internal reasoning chunk" }, { kind: "block" });
expect(streamingInstances).toHaveLength(0);
@@ -253,15 +260,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
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 } = createDispatcherHarness({
runtime: createRuntimeLogger(),
rootId: "om_root_topic",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(1);
@@ -277,14 +279,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("closes streaming with block text when final reply is missing", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```md\npartial answer\n```" }, { kind: "block" });
await options.onIdle?.();
@@ -295,14 +292,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("delivers distinct final payloads after streaming close", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```md\n完整回复第一段\n```" }, { kind: "final" });
await options.deliver({ text: "```md\n完整回复第一段 + 第二段\n```" }, { kind: "final" });
@@ -316,14 +308,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("skips exact duplicate final text after streaming close", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
@@ -383,14 +370,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
},
});
const result = createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { result, options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
await result.replyOptions.onPartialReply?.({ text: "hello" });
await options.deliver({ text: "lo world" }, { kind: "block" });
@@ -402,14 +384,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("sends media-only payloads as attachments", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
const { options } = createDispatcherHarness();
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
@@ -424,14 +399,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("falls back to legacy mediaUrl when mediaUrls is an empty array", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
const { options } = createDispatcherHarness();
await options.deliver(
{ text: "caption", mediaUrl: "https://example.com/a.png", mediaUrls: [] },
{ kind: "final" },
@@ -447,14 +415,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("sends attachments after streaming final markdown replies", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver(
{ text: "```ts\nconst x = 1\n```", mediaUrls: ["https://example.com/a.png"] },
{ kind: "final" },
@@ -472,16 +435,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("passes replyInThread to sendMessageFeishu for plain text", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
replyToMessageId: "om_msg",
replyInThread: true,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "plain text" }, { kind: "final" });
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
@@ -504,16 +461,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
},
});
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
replyToMessageId: "om_msg",
replyInThread: true,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "card text" }, { kind: "final" });
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
@@ -525,16 +476,11 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("passes replyToMessageId and replyInThread to streaming.start()", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
replyToMessageId: "om_msg",
replyInThread: true,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(1);
@@ -545,18 +491,13 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("disables streaming for thread replies and keeps reply metadata", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: { log: vi.fn(), error: vi.fn() } as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
replyToMessageId: "om_msg",
replyInThread: false,
threadReply: true,
rootId: "om_root_topic",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(0);
@@ -569,16 +510,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
it("passes replyInThread to media attachments", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
const { options } = createDispatcherHarness({
replyToMessageId: "om_msg",
replyInThread: true,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
expect(sendMediaFeishuMock).toHaveBeenCalledWith(