mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:11:25 +00:00
refactor(test): dedupe telegram dispatch scaffolding
This commit is contained in:
@@ -30,6 +30,8 @@ vi.mock("./sticker-cache.js", () => ({
|
|||||||
import { dispatchTelegramMessage } from "./bot-message-dispatch.js";
|
import { dispatchTelegramMessage } from "./bot-message-dispatch.js";
|
||||||
|
|
||||||
describe("dispatchTelegramMessage draft streaming", () => {
|
describe("dispatchTelegramMessage draft streaming", () => {
|
||||||
|
type TelegramMessageContext = Parameters<typeof dispatchTelegramMessage>[0]["context"];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createTelegramDraftStream.mockReset();
|
createTelegramDraftStream.mockReset();
|
||||||
dispatchReplyWithBufferedBlockDispatcher.mockReset();
|
dispatchReplyWithBufferedBlockDispatcher.mockReset();
|
||||||
@@ -37,25 +39,18 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
editMessageTelegram.mockReset();
|
editMessageTelegram.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("streams drafts in private threads and forwards thread id", async () => {
|
function createDraftStream(messageId?: number) {
|
||||||
const draftStream = {
|
return {
|
||||||
update: vi.fn(),
|
update: vi.fn(),
|
||||||
flush: vi.fn().mockResolvedValue(undefined),
|
flush: vi.fn().mockResolvedValue(undefined),
|
||||||
messageId: vi.fn().mockReturnValue(undefined),
|
messageId: vi.fn().mockReturnValue(messageId),
|
||||||
clear: vi.fn().mockResolvedValue(undefined),
|
clear: vi.fn().mockResolvedValue(undefined),
|
||||||
stop: vi.fn(),
|
stop: vi.fn(),
|
||||||
};
|
};
|
||||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
}
|
||||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
|
||||||
async ({ dispatcherOptions, replyOptions }) => {
|
|
||||||
await replyOptions?.onPartialReply?.({ text: "Hello" });
|
|
||||||
await dispatcherOptions.deliver({ text: "Hello" }, { kind: "final" });
|
|
||||||
return { queuedFinal: true };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
deliverReplies.mockResolvedValue({ delivered: true });
|
|
||||||
|
|
||||||
const context = {
|
function createContext(overrides?: Partial<TelegramMessageContext>): TelegramMessageContext {
|
||||||
|
const base = {
|
||||||
ctxPayload: {},
|
ctxPayload: {},
|
||||||
primaryCtx: { message: { chat: { id: 123, type: "private" } } },
|
primaryCtx: { message: { chat: { id: 123, type: "private" } } },
|
||||||
msg: {
|
msg: {
|
||||||
@@ -78,28 +73,72 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
ackReactionPromise: null,
|
ackReactionPromise: null,
|
||||||
reactionApi: null,
|
reactionApi: null,
|
||||||
removeAckAfterReply: false,
|
removeAckAfterReply: false,
|
||||||
};
|
} as unknown as TelegramMessageContext;
|
||||||
|
|
||||||
const bot = { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
return {
|
||||||
const runtime = {
|
...base,
|
||||||
|
...overrides,
|
||||||
|
// Merge nested fields when overrides provide partial objects.
|
||||||
|
primaryCtx: {
|
||||||
|
...(base.primaryCtx as object),
|
||||||
|
...(overrides?.primaryCtx ? (overrides.primaryCtx as object) : null),
|
||||||
|
} as TelegramMessageContext["primaryCtx"],
|
||||||
|
msg: {
|
||||||
|
...(base.msg as object),
|
||||||
|
...(overrides?.msg ? (overrides.msg as object) : null),
|
||||||
|
} as TelegramMessageContext["msg"],
|
||||||
|
route: {
|
||||||
|
...(base.route as object),
|
||||||
|
...(overrides?.route ? (overrides.route as object) : null),
|
||||||
|
} as TelegramMessageContext["route"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBot(): Bot {
|
||||||
|
return { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuntime(): Parameters<typeof dispatchTelegramMessage>[0]["runtime"] {
|
||||||
|
return {
|
||||||
log: vi.fn(),
|
log: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
exit: () => {
|
exit: () => {
|
||||||
throw new Error("exit");
|
throw new Error("exit");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatchWithContext(params: {
|
||||||
|
context: TelegramMessageContext;
|
||||||
|
telegramCfg?: Parameters<typeof dispatchTelegramMessage>[0]["telegramCfg"];
|
||||||
|
}) {
|
||||||
await dispatchTelegramMessage({
|
await dispatchTelegramMessage({
|
||||||
context,
|
context: params.context,
|
||||||
bot,
|
bot: createBot(),
|
||||||
cfg: {},
|
cfg: {},
|
||||||
runtime,
|
runtime: createRuntime(),
|
||||||
replyToMode: "first",
|
replyToMode: "first",
|
||||||
streamMode: "partial",
|
streamMode: "partial",
|
||||||
textLimit: 4096,
|
textLimit: 4096,
|
||||||
telegramCfg: {},
|
telegramCfg: params.telegramCfg ?? {},
|
||||||
opts: { token: "token" },
|
opts: { token: "token" },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("streams drafts in private threads and forwards thread id", async () => {
|
||||||
|
const draftStream = createDraftStream();
|
||||||
|
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||||
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||||
|
async ({ dispatcherOptions, replyOptions }) => {
|
||||||
|
await replyOptions?.onPartialReply?.({ text: "Hello" });
|
||||||
|
await dispatcherOptions.deliver({ text: "Hello" }, { kind: "final" });
|
||||||
|
return { queuedFinal: true };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
|
|
||||||
|
const context = createContext();
|
||||||
|
await dispatchWithContext({ context });
|
||||||
|
|
||||||
expect(createTelegramDraftStream).toHaveBeenCalledWith(
|
expect(createTelegramDraftStream).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -131,49 +170,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
});
|
});
|
||||||
deliverReplies.mockResolvedValue({ delivered: true });
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
|
|
||||||
const context = {
|
await dispatchWithContext({
|
||||||
ctxPayload: {},
|
context: createContext(),
|
||||||
primaryCtx: { message: { chat: { id: 123, type: "private" } } },
|
|
||||||
msg: {
|
|
||||||
chat: { id: 123, type: "private" },
|
|
||||||
message_id: 456,
|
|
||||||
message_thread_id: 777,
|
|
||||||
},
|
|
||||||
chatId: 123,
|
|
||||||
isGroup: false,
|
|
||||||
resolvedThreadId: undefined,
|
|
||||||
replyThreadId: 777,
|
|
||||||
threadSpec: { id: 777, scope: "dm" },
|
|
||||||
historyKey: undefined,
|
|
||||||
historyLimit: 0,
|
|
||||||
groupHistories: new Map(),
|
|
||||||
route: { agentId: "default", accountId: "default" },
|
|
||||||
skillFilter: undefined,
|
|
||||||
sendTyping: vi.fn(),
|
|
||||||
sendRecordVoice: vi.fn(),
|
|
||||||
ackReactionPromise: null,
|
|
||||||
reactionApi: null,
|
|
||||||
removeAckAfterReply: false,
|
|
||||||
};
|
|
||||||
const bot = { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
|
||||||
const runtime = {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: () => {
|
|
||||||
throw new Error("exit");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await dispatchTelegramMessage({
|
|
||||||
context,
|
|
||||||
bot,
|
|
||||||
cfg: {},
|
|
||||||
runtime,
|
|
||||||
replyToMode: "first",
|
|
||||||
streamMode: "partial",
|
|
||||||
textLimit: 4096,
|
|
||||||
telegramCfg: { blockStreaming: true },
|
telegramCfg: { blockStreaming: true },
|
||||||
opts: { token: "token" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createTelegramDraftStream).not.toHaveBeenCalled();
|
expect(createTelegramDraftStream).not.toHaveBeenCalled();
|
||||||
@@ -188,13 +187,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("finalizes text-only replies by editing the preview message in place", async () => {
|
it("finalizes text-only replies by editing the preview message in place", async () => {
|
||||||
const draftStream = {
|
const draftStream = createDraftStream(999);
|
||||||
update: vi.fn(),
|
|
||||||
flush: vi.fn().mockResolvedValue(undefined),
|
|
||||||
messageId: vi.fn().mockReturnValue(999),
|
|
||||||
clear: vi.fn().mockResolvedValue(undefined),
|
|
||||||
stop: vi.fn(),
|
|
||||||
};
|
|
||||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||||
async ({ dispatcherOptions, replyOptions }) => {
|
async ({ dispatcherOptions, replyOptions }) => {
|
||||||
@@ -206,51 +199,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
deliverReplies.mockResolvedValue({ delivered: true });
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
||||||
|
|
||||||
const context = {
|
await dispatchWithContext({ context: createContext() });
|
||||||
ctxPayload: {},
|
|
||||||
primaryCtx: { message: { chat: { id: 123, type: "private" } } },
|
|
||||||
msg: {
|
|
||||||
chat: { id: 123, type: "private" },
|
|
||||||
message_id: 456,
|
|
||||||
message_thread_id: 777,
|
|
||||||
},
|
|
||||||
chatId: 123,
|
|
||||||
isGroup: false,
|
|
||||||
resolvedThreadId: undefined,
|
|
||||||
replyThreadId: 777,
|
|
||||||
threadSpec: { id: 777, scope: "dm" },
|
|
||||||
historyKey: undefined,
|
|
||||||
historyLimit: 0,
|
|
||||||
groupHistories: new Map(),
|
|
||||||
route: { agentId: "default", accountId: "default" },
|
|
||||||
skillFilter: undefined,
|
|
||||||
sendTyping: vi.fn(),
|
|
||||||
sendRecordVoice: vi.fn(),
|
|
||||||
ackReactionPromise: null,
|
|
||||||
reactionApi: null,
|
|
||||||
removeAckAfterReply: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bot = { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
|
||||||
const runtime = {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: () => {
|
|
||||||
throw new Error("exit");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await dispatchTelegramMessage({
|
|
||||||
context,
|
|
||||||
bot,
|
|
||||||
cfg: {},
|
|
||||||
runtime,
|
|
||||||
replyToMode: "first",
|
|
||||||
streamMode: "partial",
|
|
||||||
textLimit: 4096,
|
|
||||||
telegramCfg: {},
|
|
||||||
opts: { token: "token" },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "Hello final", expect.any(Object));
|
expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "Hello final", expect.any(Object));
|
||||||
expect(deliverReplies).not.toHaveBeenCalled();
|
expect(deliverReplies).not.toHaveBeenCalled();
|
||||||
@@ -259,13 +208,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to normal delivery when preview final is too long to edit", async () => {
|
it("falls back to normal delivery when preview final is too long to edit", async () => {
|
||||||
const draftStream = {
|
const draftStream = createDraftStream(999);
|
||||||
update: vi.fn(),
|
|
||||||
flush: vi.fn().mockResolvedValue(undefined),
|
|
||||||
messageId: vi.fn().mockReturnValue(999),
|
|
||||||
clear: vi.fn().mockResolvedValue(undefined),
|
|
||||||
stop: vi.fn(),
|
|
||||||
};
|
|
||||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||||
const longText = "x".repeat(5000);
|
const longText = "x".repeat(5000);
|
||||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
||||||
@@ -275,51 +218,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
deliverReplies.mockResolvedValue({ delivered: true });
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
||||||
|
|
||||||
const context = {
|
await dispatchWithContext({ context: createContext() });
|
||||||
ctxPayload: {},
|
|
||||||
primaryCtx: { message: { chat: { id: 123, type: "private" } } },
|
|
||||||
msg: {
|
|
||||||
chat: { id: 123, type: "private" },
|
|
||||||
message_id: 456,
|
|
||||||
message_thread_id: 777,
|
|
||||||
},
|
|
||||||
chatId: 123,
|
|
||||||
isGroup: false,
|
|
||||||
resolvedThreadId: undefined,
|
|
||||||
replyThreadId: 777,
|
|
||||||
threadSpec: { id: 777, scope: "dm" },
|
|
||||||
historyKey: undefined,
|
|
||||||
historyLimit: 0,
|
|
||||||
groupHistories: new Map(),
|
|
||||||
route: { agentId: "default", accountId: "default" },
|
|
||||||
skillFilter: undefined,
|
|
||||||
sendTyping: vi.fn(),
|
|
||||||
sendRecordVoice: vi.fn(),
|
|
||||||
ackReactionPromise: null,
|
|
||||||
reactionApi: null,
|
|
||||||
removeAckAfterReply: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bot = { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
|
||||||
const runtime = {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: () => {
|
|
||||||
throw new Error("exit");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await dispatchTelegramMessage({
|
|
||||||
context,
|
|
||||||
bot,
|
|
||||||
cfg: {},
|
|
||||||
runtime,
|
|
||||||
replyToMode: "first",
|
|
||||||
streamMode: "partial",
|
|
||||||
textLimit: 4096,
|
|
||||||
telegramCfg: {},
|
|
||||||
opts: { token: "token" },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||||
expect(deliverReplies).toHaveBeenCalledWith(
|
expect(deliverReplies).toHaveBeenCalledWith(
|
||||||
|
|||||||
Reference in New Issue
Block a user