feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
|
|
|
import type { Bot } from "grammy";
|
2026-01-31 21:55:59 +05:30
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
const createTelegramDraftStream = vi.hoisted(() => vi.fn());
|
|
|
|
|
const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn());
|
|
|
|
|
const deliverReplies = vi.hoisted(() => vi.fn());
|
2026-02-15 20:09:10 +05:30
|
|
|
const editMessageTelegram = vi.hoisted(() => vi.fn());
|
2026-01-31 21:55:59 +05:30
|
|
|
|
|
|
|
|
vi.mock("./draft-stream.js", () => ({
|
|
|
|
|
createTelegramDraftStream,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../auto-reply/reply/provider-dispatcher.js", () => ({
|
|
|
|
|
dispatchReplyWithBufferedBlockDispatcher,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("./bot/delivery.js", () => ({
|
|
|
|
|
deliverReplies,
|
|
|
|
|
}));
|
|
|
|
|
|
2026-02-15 20:09:10 +05:30
|
|
|
vi.mock("./send.js", () => ({
|
|
|
|
|
editMessageTelegram,
|
|
|
|
|
}));
|
|
|
|
|
|
2026-01-31 21:55:59 +05:30
|
|
|
vi.mock("./sticker-cache.js", () => ({
|
|
|
|
|
cacheSticker: vi.fn(),
|
|
|
|
|
describeStickerImage: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
import { dispatchTelegramMessage } from "./bot-message-dispatch.js";
|
|
|
|
|
|
|
|
|
|
describe("dispatchTelegramMessage draft streaming", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
createTelegramDraftStream.mockReset();
|
|
|
|
|
dispatchReplyWithBufferedBlockDispatcher.mockReset();
|
|
|
|
|
deliverReplies.mockReset();
|
2026-02-15 20:09:10 +05:30
|
|
|
editMessageTelegram.mockReset();
|
2026-01-31 21:55:59 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("streams drafts in private threads and forwards thread id", async () => {
|
|
|
|
|
const draftStream = {
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
2026-02-15 20:09:10 +05:30
|
|
|
messageId: vi.fn().mockReturnValue(undefined),
|
|
|
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
2026-01-31 21:55:59 +05:30
|
|
|
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 = {
|
|
|
|
|
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,
|
2026-02-02 08:53:42 +05:30
|
|
|
threadSpec: { id: 777, scope: "dm" },
|
2026-01-31 21:55:59 +05:30
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-15 20:09:10 +05:30
|
|
|
const bot = { api: { sendMessage: vi.fn(), editMessageText: vi.fn() } } as unknown as Bot;
|
feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
|
|
|
const runtime = {
|
|
|
|
|
log: vi.fn(),
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
exit: () => {
|
|
|
|
|
throw new Error("exit");
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-31 21:55:59 +05:30
|
|
|
await dispatchTelegramMessage({
|
|
|
|
|
context,
|
feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
|
|
|
bot,
|
2026-01-31 21:55:59 +05:30
|
|
|
cfg: {},
|
feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
|
|
|
runtime,
|
2026-01-31 21:55:59 +05:30
|
|
|
replyToMode: "first",
|
|
|
|
|
streamMode: "partial",
|
|
|
|
|
textLimit: 4096,
|
|
|
|
|
telegramCfg: {},
|
feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
|
|
|
opts: { token: "token" },
|
2026-01-31 21:55:59 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(createTelegramDraftStream).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
chatId: 123,
|
2026-02-02 08:53:42 +05:30
|
|
|
thread: { id: 777, scope: "dm" },
|
2026-01-31 21:55:59 +05:30
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(draftStream.update).toHaveBeenCalledWith("Hello");
|
|
|
|
|
expect(deliverReplies).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2026-02-02 08:53:42 +05:30
|
|
|
thread: { id: 777, scope: "dm" },
|
2026-01-31 21:55:59 +05:30
|
|
|
}),
|
|
|
|
|
);
|
2026-02-15 20:09:10 +05:30
|
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
replyOptions: expect.objectContaining({
|
|
|
|
|
disableBlockStreaming: true,
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(editMessageTelegram).not.toHaveBeenCalled();
|
|
|
|
|
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps block streaming enabled when account config enables it", async () => {
|
|
|
|
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
|
|
|
|
await dispatcherOptions.deliver({ text: "Hello" }, { kind: "final" });
|
|
|
|
|
return { queuedFinal: true };
|
|
|
|
|
});
|
|
|
|
|
deliverReplies.mockResolvedValue({ delivered: true });
|
|
|
|
|
|
|
|
|
|
const context = {
|
|
|
|
|
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: { blockStreaming: true },
|
|
|
|
|
opts: { token: "token" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(createTelegramDraftStream).not.toHaveBeenCalled();
|
|
|
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
replyOptions: expect.objectContaining({
|
|
|
|
|
disableBlockStreaming: false,
|
|
|
|
|
onPartialReply: undefined,
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("finalizes text-only replies by editing the preview message in place", async () => {
|
|
|
|
|
const draftStream = {
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
messageId: vi.fn().mockReturnValue(999),
|
|
|
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
stop: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
createTelegramDraftStream.mockReturnValue(draftStream);
|
|
|
|
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
|
|
|
|
async ({ dispatcherOptions, replyOptions }) => {
|
|
|
|
|
await replyOptions?.onPartialReply?.({ text: "Hel" });
|
|
|
|
|
await dispatcherOptions.deliver({ text: "Hello final" }, { kind: "final" });
|
|
|
|
|
return { queuedFinal: true };
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
deliverReplies.mockResolvedValue({ delivered: true });
|
|
|
|
|
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
|
|
|
|
|
|
|
|
|
const context = {
|
|
|
|
|
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(deliverReplies).not.toHaveBeenCalled();
|
|
|
|
|
expect(draftStream.clear).not.toHaveBeenCalled();
|
|
|
|
|
expect(draftStream.stop).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("falls back to normal delivery when preview final is too long to edit", async () => {
|
|
|
|
|
const draftStream = {
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
messageId: vi.fn().mockReturnValue(999),
|
|
|
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
stop: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
createTelegramDraftStream.mockReturnValue(draftStream);
|
|
|
|
|
const longText = "x".repeat(5000);
|
|
|
|
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
|
|
|
|
await dispatcherOptions.deliver({ text: longText }, { kind: "final" });
|
|
|
|
|
return { queuedFinal: true };
|
|
|
|
|
});
|
|
|
|
|
deliverReplies.mockResolvedValue({ delivered: true });
|
|
|
|
|
editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" });
|
|
|
|
|
|
|
|
|
|
const context = {
|
|
|
|
|
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(deliverReplies).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
replies: [expect.objectContaining({ text: longText })],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(draftStream.stop).toHaveBeenCalled();
|
2026-01-31 21:55:59 +05:30
|
|
|
});
|
|
|
|
|
});
|