test: reduce feishu reply dispatcher duplication
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user