@ai-devkit/channel-connector 0.13.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ChannelConfigRepository.d.ts +1 -1
- package/dist/ChannelConfigRepository.d.ts.map +1 -1
- package/dist/ChannelConfigRepository.js +13 -13
- package/dist/ChannelConfigRepository.js.map +1 -1
- package/dist/ChannelManager.d.ts +1 -1
- package/dist/ChannelManager.d.ts.map +1 -1
- package/dist/ChannelManager.js +9 -9
- package/dist/ChannelManager.js.map +1 -1
- package/dist/__tests__/ChannelConfigRepository.test.js +72 -72
- package/dist/__tests__/ChannelConfigRepository.test.js.map +1 -1
- package/dist/__tests__/ChannelManager.test.js +26 -26
- package/dist/__tests__/ChannelManager.test.js.map +1 -1
- package/dist/__tests__/adapters/slack/SlackAdapter.test.js +137 -137
- package/dist/__tests__/adapters/slack/SlackAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/slack/SlackMessageDelivery.test.js +36 -36
- package/dist/__tests__/adapters/slack/SlackMessageDelivery.test.js.map +1 -1
- package/dist/__tests__/adapters/slack/slackMarkdown.test.js +25 -25
- package/dist/__tests__/adapters/slack/slackMarkdown.test.js.map +1 -1
- package/dist/__tests__/adapters/slack/slackQuestion.test.js +41 -41
- package/dist/__tests__/adapters/slack/slackQuestion.test.js.map +1 -1
- package/dist/__tests__/adapters/telegram/TelegramAdapter.test.js +177 -154
- package/dist/__tests__/adapters/telegram/TelegramAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/telegram/telegramHtml.test.js +109 -109
- package/dist/__tests__/adapters/telegram/telegramHtml.test.js.map +1 -1
- package/dist/adapters/ChannelAdapter.d.ts +1 -1
- package/dist/adapters/ChannelAdapter.d.ts.map +1 -1
- package/dist/adapters/ChannelAdapter.js +1 -1
- package/dist/adapters/ChannelAdapter.js.map +1 -1
- package/dist/adapters/slack/SlackAdapter.d.ts +2 -2
- package/dist/adapters/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/adapters/slack/SlackAdapter.js +17 -17
- package/dist/adapters/slack/SlackAdapter.js.map +1 -1
- package/dist/adapters/slack/SlackMessageDelivery.d.ts +1 -1
- package/dist/adapters/slack/SlackMessageDelivery.d.ts.map +1 -1
- package/dist/adapters/slack/SlackMessageDelivery.js +6 -6
- package/dist/adapters/slack/SlackMessageDelivery.js.map +1 -1
- package/dist/adapters/slack/slackMarkdown.d.ts.map +1 -1
- package/dist/adapters/slack/slackMarkdown.js +36 -36
- package/dist/adapters/slack/slackMarkdown.js.map +1 -1
- package/dist/adapters/slack/slackQuestion.d.ts +7 -7
- package/dist/adapters/slack/slackQuestion.d.ts.map +1 -1
- package/dist/adapters/slack/slackQuestion.js +13 -13
- package/dist/adapters/slack/slackQuestion.js.map +1 -1
- package/dist/adapters/telegram/TelegramAdapter.d.ts +12 -2
- package/dist/adapters/telegram/TelegramAdapter.d.ts.map +1 -1
- package/dist/adapters/telegram/TelegramAdapter.js +38 -27
- package/dist/adapters/telegram/TelegramAdapter.js.map +1 -1
- package/dist/adapters/telegram/TelegramMessageDelivery.d.ts.map +1 -1
- package/dist/adapters/telegram/TelegramMessageDelivery.js +2 -2
- package/dist/adapters/telegram/TelegramMessageDelivery.js.map +1 -1
- package/dist/adapters/telegram/telegramHtml.d.ts.map +1 -1
- package/dist/adapters/telegram/telegramHtml.js +16 -16
- package/dist/adapters/telegram/telegramHtml.js.map +1 -1
- package/dist/adapters/telegram/telegramKeyboard.d.ts +1 -1
- package/dist/adapters/telegram/telegramKeyboard.d.ts.map +1 -1
- package/dist/adapters/telegram/telegramKeyboard.js.map +1 -1
- package/dist/adapters/telegram/telegramMarkdown.d.ts.map +1 -1
- package/dist/adapters/telegram/telegramMarkdown.js +28 -28
- package/dist/adapters/telegram/telegramMarkdown.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +6 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +21 -22
- package/vitest.config.ts +16 -16
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as telegrafModule from
|
|
2
|
-
import * as telegramHtml from
|
|
3
|
-
import { TelegramAdapter } from
|
|
1
|
+
import * as telegrafModule from "telegraf";
|
|
2
|
+
import * as telegramHtml from "../../../adapters/telegram/telegramHtml.js";
|
|
3
|
+
import { TelegramAdapter, validateTelegramBotToken } from "../../../adapters/telegram/TelegramAdapter.js";
|
|
4
4
|
// Mock telegraf
|
|
5
|
-
vi.mock(
|
|
5
|
+
vi.mock("telegraf", ()=>{
|
|
6
6
|
const handlers = {};
|
|
7
7
|
const mockBot = {
|
|
8
8
|
launch: vi.fn().mockResolvedValue(undefined),
|
|
@@ -15,7 +15,7 @@ vi.mock('telegraf', ()=>{
|
|
|
15
15
|
editMessageReplyMarkup: vi.fn().mockResolvedValue(undefined),
|
|
16
16
|
answerCbQuery: vi.fn().mockResolvedValue(undefined),
|
|
17
17
|
getMe: vi.fn().mockResolvedValue({
|
|
18
|
-
username:
|
|
18
|
+
username: "test_bot"
|
|
19
19
|
})
|
|
20
20
|
},
|
|
21
21
|
_handlers: handlers,
|
|
@@ -33,15 +33,15 @@ vi.mock('telegraf', ()=>{
|
|
|
33
33
|
},
|
|
34
34
|
reply: vi.fn().mockResolvedValue(undefined)
|
|
35
35
|
};
|
|
36
|
-
if (handlers[
|
|
37
|
-
await handlers[
|
|
36
|
+
if (handlers["text"]) {
|
|
37
|
+
await handlers["text"](ctx);
|
|
38
38
|
}
|
|
39
39
|
return ctx;
|
|
40
40
|
},
|
|
41
41
|
_triggerCallback: async (chatId, userId, data, messageId = 555)=>{
|
|
42
42
|
const ctx = {
|
|
43
43
|
callbackQuery: {
|
|
44
|
-
id:
|
|
44
|
+
id: "cbq-test",
|
|
45
45
|
data,
|
|
46
46
|
message: {
|
|
47
47
|
message_id: messageId,
|
|
@@ -55,8 +55,8 @@ vi.mock('telegraf', ()=>{
|
|
|
55
55
|
},
|
|
56
56
|
answerCbQuery: vi.fn().mockResolvedValue(undefined)
|
|
57
57
|
};
|
|
58
|
-
if (handlers[
|
|
59
|
-
await handlers[
|
|
58
|
+
if (handlers["callback_query"]) {
|
|
59
|
+
await handlers["callback_query"](ctx);
|
|
60
60
|
}
|
|
61
61
|
return ctx;
|
|
62
62
|
}
|
|
@@ -77,213 +77,236 @@ function expectHtmlChunkWithinLimit(chunk) {
|
|
|
77
77
|
expect((chunk.match(/<pre>/g) ?? []).length).toBe((chunk.match(/<\/pre>/g) ?? []).length);
|
|
78
78
|
expect((chunk.match(/<code(?: class="language-[^"]+")?>/g) ?? []).length).toBe((chunk.match(/<\/code>/g) ?? []).length);
|
|
79
79
|
}
|
|
80
|
-
describe(
|
|
80
|
+
describe("TelegramAdapter", ()=>{
|
|
81
|
+
it("returns the bot username for a valid token", async ()=>{
|
|
82
|
+
const getMe = vi.fn().mockResolvedValue({
|
|
83
|
+
username: "test_bot"
|
|
84
|
+
});
|
|
85
|
+
await expect(validateTelegramBotToken("123:abc", {
|
|
86
|
+
getMe
|
|
87
|
+
})).resolves.toEqual({
|
|
88
|
+
username: "test_bot"
|
|
89
|
+
});
|
|
90
|
+
expect(getMe).toHaveBeenCalledOnce();
|
|
91
|
+
});
|
|
92
|
+
it("rejects when the API reports no username", async ()=>{
|
|
93
|
+
const getMe = vi.fn().mockResolvedValue({});
|
|
94
|
+
await expect(validateTelegramBotToken("123:abc", {
|
|
95
|
+
getMe
|
|
96
|
+
})).rejects.toThrow("Telegram bot token returned no username");
|
|
97
|
+
});
|
|
98
|
+
it("propagates API errors such as 401 Unauthorized", async ()=>{
|
|
99
|
+
const getMe = vi.fn().mockRejectedValue(new Error("401: Unauthorized"));
|
|
100
|
+
await expect(validateTelegramBotToken("123:abc", {
|
|
101
|
+
getMe
|
|
102
|
+
})).rejects.toThrow("401: Unauthorized");
|
|
103
|
+
});
|
|
81
104
|
let adapter;
|
|
82
105
|
beforeEach(()=>{
|
|
83
106
|
vi.clearAllMocks();
|
|
84
107
|
adapter = new TelegramAdapter({
|
|
85
|
-
botToken:
|
|
108
|
+
botToken: "test-token-123"
|
|
86
109
|
});
|
|
87
110
|
});
|
|
88
|
-
describe(
|
|
111
|
+
describe("type", ()=>{
|
|
89
112
|
it('should return "telegram"', ()=>{
|
|
90
|
-
expect(adapter.type).toBe(
|
|
113
|
+
expect(adapter.type).toBe("telegram");
|
|
91
114
|
});
|
|
92
115
|
});
|
|
93
|
-
describe(
|
|
94
|
-
it(
|
|
116
|
+
describe("start", ()=>{
|
|
117
|
+
it("should launch the telegraf bot", async ()=>{
|
|
95
118
|
const bot = getMockBot();
|
|
96
119
|
await adapter.start();
|
|
97
120
|
expect(bot.launch).toHaveBeenCalled();
|
|
98
121
|
});
|
|
99
122
|
});
|
|
100
|
-
describe(
|
|
101
|
-
it(
|
|
123
|
+
describe("stop", ()=>{
|
|
124
|
+
it("should stop the telegraf bot", async ()=>{
|
|
102
125
|
const bot = getMockBot();
|
|
103
126
|
await adapter.start();
|
|
104
127
|
await adapter.stop();
|
|
105
128
|
expect(bot.stop).toHaveBeenCalled();
|
|
106
129
|
});
|
|
107
130
|
});
|
|
108
|
-
describe(
|
|
109
|
-
it(
|
|
131
|
+
describe("onMessage", ()=>{
|
|
132
|
+
it("should silently ignore messages when no handler is registered", async ()=>{
|
|
110
133
|
// Don't register a handler
|
|
111
134
|
await adapter.start();
|
|
112
135
|
const bot = getMockBot();
|
|
113
|
-
const ctx = await bot._triggerText(12345, 67890,
|
|
136
|
+
const ctx = await bot._triggerText(12345, 67890, "hello");
|
|
114
137
|
// Should not throw or reply
|
|
115
138
|
expect(ctx.reply).not.toHaveBeenCalled();
|
|
116
139
|
});
|
|
117
|
-
it(
|
|
118
|
-
const handler = vi.fn().mockRejectedValue(
|
|
140
|
+
it("should handle non-Error thrown by handler", async ()=>{
|
|
141
|
+
const handler = vi.fn().mockRejectedValue("string error");
|
|
119
142
|
adapter.onMessage(handler);
|
|
120
143
|
await adapter.start();
|
|
121
144
|
const bot = getMockBot();
|
|
122
|
-
const ctx = await bot._triggerText(12345, 67890,
|
|
123
|
-
expect(ctx.reply).toHaveBeenCalledWith(
|
|
145
|
+
const ctx = await bot._triggerText(12345, 67890, "hello");
|
|
146
|
+
expect(ctx.reply).toHaveBeenCalledWith("Error processing message: Unknown error");
|
|
124
147
|
});
|
|
125
|
-
it(
|
|
148
|
+
it("should call handler with IncomingMessage on incoming text", async ()=>{
|
|
126
149
|
const handler = vi.fn().mockResolvedValue(undefined);
|
|
127
150
|
adapter.onMessage(handler);
|
|
128
151
|
await adapter.start();
|
|
129
152
|
const bot = getMockBot();
|
|
130
|
-
await bot._triggerText(12345, 67890,
|
|
153
|
+
await bot._triggerText(12345, 67890, "hello agent");
|
|
131
154
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
132
155
|
const msg = handler.mock.calls[0][0];
|
|
133
|
-
expect(msg.channelType).toBe(
|
|
134
|
-
expect(msg.chatId).toBe(
|
|
135
|
-
expect(msg.userId).toBe(
|
|
136
|
-
expect(msg.text).toBe(
|
|
156
|
+
expect(msg.channelType).toBe("telegram");
|
|
157
|
+
expect(msg.chatId).toBe("12345");
|
|
158
|
+
expect(msg.userId).toBe("67890");
|
|
159
|
+
expect(msg.text).toBe("hello agent");
|
|
137
160
|
expect(msg.timestamp).toBeInstanceOf(Date);
|
|
138
161
|
});
|
|
139
|
-
it(
|
|
140
|
-
const handler = vi.fn().mockRejectedValue(new Error(
|
|
162
|
+
it("should handle handler errors gracefully", async ()=>{
|
|
163
|
+
const handler = vi.fn().mockRejectedValue(new Error("handler failed"));
|
|
141
164
|
adapter.onMessage(handler);
|
|
142
165
|
await adapter.start();
|
|
143
166
|
const bot = getMockBot();
|
|
144
|
-
const ctx = await bot._triggerText(12345, 67890,
|
|
167
|
+
const ctx = await bot._triggerText(12345, 67890, "hello");
|
|
145
168
|
// Should not throw, and should reply with error
|
|
146
|
-
expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining(
|
|
169
|
+
expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining("Error processing message"));
|
|
147
170
|
});
|
|
148
171
|
});
|
|
149
|
-
describe(
|
|
150
|
-
it(
|
|
172
|
+
describe("sendMessage", ()=>{
|
|
173
|
+
it("should send plain text with parse_mode HTML", async ()=>{
|
|
151
174
|
const bot = getMockBot();
|
|
152
|
-
await adapter.sendMessage(
|
|
153
|
-
expect(bot.telegram.sendMessage).toHaveBeenCalledWith(
|
|
154
|
-
parse_mode:
|
|
175
|
+
await adapter.sendMessage("12345", "hello from bot");
|
|
176
|
+
expect(bot.telegram.sendMessage).toHaveBeenCalledWith("12345", "hello from bot", {
|
|
177
|
+
parse_mode: "HTML"
|
|
155
178
|
});
|
|
156
179
|
});
|
|
157
|
-
it(
|
|
180
|
+
it("should render markdown as Telegram HTML", async ()=>{
|
|
158
181
|
const bot = getMockBot();
|
|
159
|
-
await adapter.sendMessage(
|
|
182
|
+
await adapter.sendMessage("12345", "**bold** and *italic* and `code`");
|
|
160
183
|
const sent = bot.telegram.sendMessage.mock.calls[0][1];
|
|
161
|
-
expect(sent).toContain(
|
|
162
|
-
expect(sent).toContain(
|
|
163
|
-
expect(sent).toContain(
|
|
184
|
+
expect(sent).toContain("<b>bold</b>");
|
|
185
|
+
expect(sent).toContain("<i>italic</i>");
|
|
186
|
+
expect(sent).toContain("<code>code</code>");
|
|
164
187
|
expect(bot.telegram.sendMessage.mock.calls[0][2]).toEqual({
|
|
165
|
-
parse_mode:
|
|
188
|
+
parse_mode: "HTML"
|
|
166
189
|
});
|
|
167
190
|
});
|
|
168
|
-
it(
|
|
191
|
+
it("should chunk messages exceeding 4096 chars", async ()=>{
|
|
169
192
|
const bot = getMockBot();
|
|
170
|
-
const line =
|
|
193
|
+
const line = "A".repeat(100) + "\n";
|
|
171
194
|
const longMessage = line.repeat(50); // 5050 chars
|
|
172
|
-
await adapter.sendMessage(
|
|
195
|
+
await adapter.sendMessage("12345", longMessage);
|
|
173
196
|
expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1);
|
|
174
197
|
for (const call of bot.telegram.sendMessage.mock.calls){
|
|
175
198
|
expect(call[1].length).toBeLessThanOrEqual(4096);
|
|
176
199
|
expect(call[2]).toEqual({
|
|
177
|
-
parse_mode:
|
|
200
|
+
parse_mode: "HTML"
|
|
178
201
|
});
|
|
179
202
|
}
|
|
180
203
|
});
|
|
181
|
-
it(
|
|
204
|
+
it("should split long fenced code into independently valid HTML code blocks", async ()=>{
|
|
182
205
|
const bot = getMockBot();
|
|
183
206
|
const codeLine = 'const sample = "<code>tag</code>" && value < limit;\n';
|
|
184
207
|
const longMessage = `Before\n\n\`\`\`ts\n${codeLine.repeat(100)}\`\`\`\n\nAfter`;
|
|
185
|
-
await adapter.sendMessage(
|
|
208
|
+
await adapter.sendMessage("12345", longMessage);
|
|
186
209
|
expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1);
|
|
187
210
|
for (const call of bot.telegram.sendMessage.mock.calls){
|
|
188
211
|
const chunk = call[1];
|
|
189
212
|
expect(call[2]).toEqual({
|
|
190
|
-
parse_mode:
|
|
213
|
+
parse_mode: "HTML"
|
|
191
214
|
});
|
|
192
215
|
expectHtmlChunkWithinLimit(chunk);
|
|
193
|
-
if (chunk.includes(
|
|
216
|
+
if (chunk.includes("<pre>")) {
|
|
194
217
|
expect(chunk).toContain('<code class="language-ts">');
|
|
195
|
-
expect(chunk).toContain(
|
|
218
|
+
expect(chunk).toContain("<code>tag</code>");
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
});
|
|
199
|
-
it(
|
|
222
|
+
it("should split nested list fenced code without sending partial HTML tags", async ()=>{
|
|
200
223
|
const bot = getMockBot();
|
|
201
224
|
const codeLine = ' return "<code>tag</code>" && input < output;\n';
|
|
202
225
|
const longMessage = [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
226
|
+
"- parent item",
|
|
227
|
+
" - child item with code",
|
|
228
|
+
" ```js",
|
|
206
229
|
codeLine.repeat(100).trimEnd(),
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
].join(
|
|
210
|
-
await adapter.sendMessage(
|
|
230
|
+
" ```",
|
|
231
|
+
"- final item"
|
|
232
|
+
].join("\n");
|
|
233
|
+
await adapter.sendMessage("12345", longMessage);
|
|
211
234
|
expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1);
|
|
212
235
|
for (const call of bot.telegram.sendMessage.mock.calls){
|
|
213
236
|
const chunk = call[1];
|
|
214
237
|
expect(call[2]).toEqual({
|
|
215
|
-
parse_mode:
|
|
238
|
+
parse_mode: "HTML"
|
|
216
239
|
});
|
|
217
240
|
expectHtmlChunkWithinLimit(chunk);
|
|
218
241
|
}
|
|
219
242
|
});
|
|
220
|
-
it(
|
|
243
|
+
it("should split long paragraphs at readable markdown boundaries", async ()=>{
|
|
221
244
|
const bot = getMockBot();
|
|
222
|
-
const sentence =
|
|
245
|
+
const sentence = "This is a long sentence with enough words to make chunking choose sentence boundaries. ";
|
|
223
246
|
const longMessage = sentence.repeat(80);
|
|
224
|
-
await adapter.sendMessage(
|
|
247
|
+
await adapter.sendMessage("12345", longMessage);
|
|
225
248
|
expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1);
|
|
226
249
|
for (const call of bot.telegram.sendMessage.mock.calls){
|
|
227
250
|
const chunk = call[1];
|
|
228
251
|
expect(call[2]).toEqual({
|
|
229
|
-
parse_mode:
|
|
252
|
+
parse_mode: "HTML"
|
|
230
253
|
});
|
|
231
254
|
expectHtmlChunkWithinLimit(chunk);
|
|
232
|
-
expect(chunk.endsWith(
|
|
255
|
+
expect(chunk.endsWith(".") || chunk.endsWith(".\n\n") || chunk.endsWith(" ")).toBe(true);
|
|
233
256
|
}
|
|
234
257
|
});
|
|
235
|
-
it(
|
|
258
|
+
it("should keep emoji chunks within the configured JavaScript string length limit", async ()=>{
|
|
236
259
|
const bot = getMockBot();
|
|
237
|
-
const longMessage = `Status ${
|
|
238
|
-
await adapter.sendMessage(
|
|
260
|
+
const longMessage = `Status ${"🧪".repeat(2500)}`;
|
|
261
|
+
await adapter.sendMessage("12345", longMessage);
|
|
239
262
|
expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1);
|
|
240
263
|
for (const call of bot.telegram.sendMessage.mock.calls){
|
|
241
264
|
expect(call[1].length).toBeLessThanOrEqual(4096);
|
|
242
265
|
expect(call[2]).toEqual({
|
|
243
|
-
parse_mode:
|
|
266
|
+
parse_mode: "HTML"
|
|
244
267
|
});
|
|
245
268
|
}
|
|
246
269
|
});
|
|
247
|
-
it(
|
|
270
|
+
it("should hard split at 4096 when no newlines available", async ()=>{
|
|
248
271
|
const bot = getMockBot();
|
|
249
|
-
const longMessage =
|
|
250
|
-
await adapter.sendMessage(
|
|
272
|
+
const longMessage = "A".repeat(5000);
|
|
273
|
+
await adapter.sendMessage("12345", longMessage);
|
|
251
274
|
expect(bot.telegram.sendMessage.mock.calls.length).toBe(2);
|
|
252
275
|
expect(bot.telegram.sendMessage.mock.calls[0][1].length).toBe(4096);
|
|
253
276
|
expect(bot.telegram.sendMessage.mock.calls[1][1].length).toBe(904);
|
|
254
277
|
});
|
|
255
|
-
it(
|
|
278
|
+
it("should prefer paragraph (\\n\\n) over single \\n when chunking", async ()=>{
|
|
256
279
|
const bot = getMockBot();
|
|
257
280
|
// 4 paragraphs of ~1500 chars each, total > 4096
|
|
258
|
-
const paragraph =
|
|
281
|
+
const paragraph = "A".repeat(1500);
|
|
259
282
|
const message = `${paragraph}\n\n${paragraph}\n\n${paragraph}\n\n${paragraph}`;
|
|
260
|
-
await adapter.sendMessage(
|
|
283
|
+
await adapter.sendMessage("12345", message);
|
|
261
284
|
// First chunk should end at a \n\n boundary, not mid-paragraph
|
|
262
285
|
const firstChunk = bot.telegram.sendMessage.mock.calls[0][1];
|
|
263
286
|
expect(firstChunk).toBe(`${paragraph}\n\n${paragraph}`);
|
|
264
287
|
});
|
|
265
|
-
it(
|
|
288
|
+
it("should send short messages in a single call", async ()=>{
|
|
266
289
|
const bot = getMockBot();
|
|
267
|
-
await adapter.sendMessage(
|
|
290
|
+
await adapter.sendMessage("12345", "short message");
|
|
268
291
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(1);
|
|
269
292
|
});
|
|
270
|
-
it(
|
|
293
|
+
it("should retry as plain text when Telegram rejects HTML with parse-entities error", async ()=>{
|
|
271
294
|
const bot = getMockBot();
|
|
272
|
-
const parseError = Object.assign(new Error(
|
|
273
|
-
description:
|
|
295
|
+
const parseError = Object.assign(new Error("400: Bad Request"), {
|
|
296
|
+
description: 'Bad Request: can\'t parse entities: Unsupported start tag "foo"'
|
|
274
297
|
});
|
|
275
298
|
bot.telegram.sendMessage.mockRejectedValueOnce(parseError).mockResolvedValueOnce(undefined);
|
|
276
|
-
await adapter.sendMessage(
|
|
299
|
+
await adapter.sendMessage("12345", "**hello**");
|
|
277
300
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(2);
|
|
278
301
|
// First call: rendered HTML with parse_mode
|
|
279
302
|
const [, htmlChunk, htmlOpts] = bot.telegram.sendMessage.mock.calls[0];
|
|
280
|
-
expect(htmlChunk).toContain(
|
|
303
|
+
expect(htmlChunk).toContain("<b>hello</b>");
|
|
281
304
|
expect(htmlOpts).toEqual({
|
|
282
|
-
parse_mode:
|
|
305
|
+
parse_mode: "HTML"
|
|
283
306
|
});
|
|
284
307
|
// Second call: same content, plain text (tags stripped, no parse_mode)
|
|
285
308
|
const [, plainChunk, plainOpts] = bot.telegram.sendMessage.mock.calls[1];
|
|
286
|
-
expect(plainChunk).toBe(
|
|
309
|
+
expect(plainChunk).toBe("hello");
|
|
287
310
|
expect(plainOpts).toBeUndefined();
|
|
288
311
|
});
|
|
289
312
|
it('should detect parse-entities error from "message" field too', async ()=>{
|
|
@@ -291,86 +314,86 @@ describe('TelegramAdapter', ()=>{
|
|
|
291
314
|
// Some error shapes carry the marker on `message` rather than `description`
|
|
292
315
|
const parseError = new Error("can't parse entities");
|
|
293
316
|
bot.telegram.sendMessage.mockRejectedValueOnce(parseError).mockResolvedValueOnce(undefined);
|
|
294
|
-
await adapter.sendMessage(
|
|
317
|
+
await adapter.sendMessage("12345", "**hi**");
|
|
295
318
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(2);
|
|
296
319
|
});
|
|
297
|
-
it(
|
|
320
|
+
it("should decode HTML entities when falling back to plain text", async ()=>{
|
|
298
321
|
const bot = getMockBot();
|
|
299
|
-
const parseError = Object.assign(new Error(
|
|
322
|
+
const parseError = Object.assign(new Error("400"), {
|
|
300
323
|
description: "Bad Request: can't parse entities"
|
|
301
324
|
});
|
|
302
325
|
bot.telegram.sendMessage.mockRejectedValueOnce(parseError).mockResolvedValueOnce(undefined);
|
|
303
326
|
// Source has chars that escapeHtml encodes; fallback should decode them
|
|
304
|
-
await adapter.sendMessage(
|
|
327
|
+
await adapter.sendMessage("12345", "a < b && c > d");
|
|
305
328
|
const [, plainChunk] = bot.telegram.sendMessage.mock.calls[1];
|
|
306
|
-
expect(plainChunk).toContain(
|
|
307
|
-
expect(plainChunk).not.toContain(
|
|
308
|
-
expect(plainChunk).not.toContain(
|
|
329
|
+
expect(plainChunk).toContain("a < b && c > d");
|
|
330
|
+
expect(plainChunk).not.toContain("<");
|
|
331
|
+
expect(plainChunk).not.toContain("&");
|
|
309
332
|
});
|
|
310
|
-
it(
|
|
333
|
+
it("should deliver nested lists without throwing", async ()=>{
|
|
311
334
|
const bot = getMockBot();
|
|
312
|
-
await adapter.sendMessage(
|
|
335
|
+
await adapter.sendMessage("12345", "- agent\n - Status");
|
|
313
336
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(1);
|
|
314
|
-
expect(bot.telegram.sendMessage.mock.calls[0][1]).toContain(
|
|
337
|
+
expect(bot.telegram.sendMessage.mock.calls[0][1]).toContain("Status");
|
|
315
338
|
});
|
|
316
|
-
it(
|
|
339
|
+
it("should send plain text when markdown formatting throws", async ()=>{
|
|
317
340
|
const bot = getMockBot();
|
|
318
|
-
const formatter = vi.spyOn(telegramHtml,
|
|
319
|
-
throw new Error(
|
|
341
|
+
const formatter = vi.spyOn(telegramHtml, "markdownToTelegramHtml").mockImplementationOnce(()=>{
|
|
342
|
+
throw new Error("formatter failed");
|
|
320
343
|
});
|
|
321
|
-
await adapter.sendMessage(
|
|
344
|
+
await adapter.sendMessage("12345", "- agent\n - Status");
|
|
322
345
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(1);
|
|
323
|
-
expect(bot.telegram.sendMessage.mock.calls[0][0]).toBe(
|
|
324
|
-
expect(bot.telegram.sendMessage.mock.calls[0][1]).toBe(
|
|
346
|
+
expect(bot.telegram.sendMessage.mock.calls[0][0]).toBe("12345");
|
|
347
|
+
expect(bot.telegram.sendMessage.mock.calls[0][1]).toBe("- agent\n - Status");
|
|
325
348
|
expect(bot.telegram.sendMessage.mock.calls[0][2]).toBeUndefined();
|
|
326
349
|
formatter.mockRestore();
|
|
327
350
|
});
|
|
328
|
-
it(
|
|
351
|
+
it("should propagate non-parse-entities errors without falling back", async ()=>{
|
|
329
352
|
const bot = getMockBot();
|
|
330
|
-
const otherError = Object.assign(new Error(
|
|
331
|
-
description:
|
|
353
|
+
const otherError = Object.assign(new Error("403"), {
|
|
354
|
+
description: "Forbidden: bot was blocked by the user"
|
|
332
355
|
});
|
|
333
356
|
bot.telegram.sendMessage.mockRejectedValueOnce(otherError);
|
|
334
|
-
await expect(adapter.sendMessage(
|
|
357
|
+
await expect(adapter.sendMessage("12345", "hi")).rejects.toBe(otherError);
|
|
335
358
|
// Only the HTML attempt should have happened — no fallback retry
|
|
336
359
|
expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(1);
|
|
337
360
|
});
|
|
338
361
|
});
|
|
339
|
-
describe(
|
|
340
|
-
it(
|
|
362
|
+
describe("sendInlineKeyboard", ()=>{
|
|
363
|
+
it("sends with parse_mode HTML and inline_keyboard payload, returns message_id", async ()=>{
|
|
341
364
|
const bot = getMockBot();
|
|
342
365
|
bot.telegram.sendMessage.mockResolvedValueOnce({
|
|
343
366
|
message_id: 42
|
|
344
367
|
});
|
|
345
|
-
const id = await adapter.sendInlineKeyboard(
|
|
368
|
+
const id = await adapter.sendInlineKeyboard("12345", "<b>hi</b>", [
|
|
346
369
|
[
|
|
347
370
|
{
|
|
348
|
-
text:
|
|
349
|
-
callbackData:
|
|
371
|
+
text: "A",
|
|
372
|
+
callbackData: "q:1:o:0"
|
|
350
373
|
}
|
|
351
374
|
],
|
|
352
375
|
[
|
|
353
376
|
{
|
|
354
|
-
text:
|
|
355
|
-
callbackData:
|
|
377
|
+
text: "B",
|
|
378
|
+
callbackData: "q:1:o:1"
|
|
356
379
|
}
|
|
357
380
|
]
|
|
358
381
|
]);
|
|
359
382
|
expect(id).toBe(42);
|
|
360
|
-
expect(bot.telegram.sendMessage).toHaveBeenCalledWith(
|
|
361
|
-
parse_mode:
|
|
383
|
+
expect(bot.telegram.sendMessage).toHaveBeenCalledWith("12345", "<b>hi</b>", {
|
|
384
|
+
parse_mode: "HTML",
|
|
362
385
|
reply_markup: {
|
|
363
386
|
inline_keyboard: [
|
|
364
387
|
[
|
|
365
388
|
{
|
|
366
|
-
text:
|
|
367
|
-
callback_data:
|
|
389
|
+
text: "A",
|
|
390
|
+
callback_data: "q:1:o:0"
|
|
368
391
|
}
|
|
369
392
|
],
|
|
370
393
|
[
|
|
371
394
|
{
|
|
372
|
-
text:
|
|
373
|
-
callback_data:
|
|
395
|
+
text: "B",
|
|
396
|
+
callback_data: "q:1:o:1"
|
|
374
397
|
}
|
|
375
398
|
]
|
|
376
399
|
]
|
|
@@ -378,80 +401,80 @@ describe('TelegramAdapter', ()=>{
|
|
|
378
401
|
});
|
|
379
402
|
});
|
|
380
403
|
});
|
|
381
|
-
describe(
|
|
382
|
-
it(
|
|
404
|
+
describe("editInlineKeyboard", ()=>{
|
|
405
|
+
it("replaces the keyboard on an existing message", async ()=>{
|
|
383
406
|
const bot = getMockBot();
|
|
384
|
-
await adapter.editInlineKeyboard(
|
|
407
|
+
await adapter.editInlineKeyboard("12345", 99, [
|
|
385
408
|
[
|
|
386
409
|
{
|
|
387
|
-
text:
|
|
388
|
-
callbackData:
|
|
410
|
+
text: "✓ A",
|
|
411
|
+
callbackData: "q:1:o:0"
|
|
389
412
|
}
|
|
390
413
|
]
|
|
391
414
|
]);
|
|
392
|
-
expect(bot.telegram.editMessageReplyMarkup).toHaveBeenCalledWith(
|
|
415
|
+
expect(bot.telegram.editMessageReplyMarkup).toHaveBeenCalledWith("12345", 99, undefined, {
|
|
393
416
|
inline_keyboard: [
|
|
394
417
|
[
|
|
395
418
|
{
|
|
396
|
-
text:
|
|
397
|
-
callback_data:
|
|
419
|
+
text: "✓ A",
|
|
420
|
+
callback_data: "q:1:o:0"
|
|
398
421
|
}
|
|
399
422
|
]
|
|
400
423
|
]
|
|
401
424
|
});
|
|
402
425
|
});
|
|
403
|
-
it(
|
|
426
|
+
it("passes undefined reply_markup to remove the keyboard", async ()=>{
|
|
404
427
|
const bot = getMockBot();
|
|
405
|
-
await adapter.editInlineKeyboard(
|
|
406
|
-
expect(bot.telegram.editMessageReplyMarkup).toHaveBeenCalledWith(
|
|
428
|
+
await adapter.editInlineKeyboard("12345", 99, null);
|
|
429
|
+
expect(bot.telegram.editMessageReplyMarkup).toHaveBeenCalledWith("12345", 99, undefined, undefined);
|
|
407
430
|
});
|
|
408
431
|
});
|
|
409
|
-
describe(
|
|
410
|
-
it(
|
|
432
|
+
describe("answerCallback", ()=>{
|
|
433
|
+
it("forwards id and optional text to telegraf", async ()=>{
|
|
411
434
|
const bot = getMockBot();
|
|
412
|
-
await adapter.answerCallback(
|
|
413
|
-
expect(bot.telegram.answerCbQuery).toHaveBeenCalledWith(
|
|
435
|
+
await adapter.answerCallback("cbq-1", "thanks");
|
|
436
|
+
expect(bot.telegram.answerCbQuery).toHaveBeenCalledWith("cbq-1", "thanks");
|
|
414
437
|
});
|
|
415
438
|
});
|
|
416
|
-
describe(
|
|
417
|
-
it(
|
|
439
|
+
describe("onCallback", ()=>{
|
|
440
|
+
it("invokes the registered handler with a normalized IncomingCallback", async ()=>{
|
|
418
441
|
const handler = vi.fn().mockResolvedValue(undefined);
|
|
419
442
|
adapter.onCallback(handler);
|
|
420
443
|
await adapter.start();
|
|
421
444
|
const bot = getMockBot();
|
|
422
|
-
await bot._triggerCallback(12345, 67890,
|
|
445
|
+
await bot._triggerCallback(12345, 67890, "q:abc:o:1", 777);
|
|
423
446
|
expect(handler).toHaveBeenCalledOnce();
|
|
424
447
|
const cb = handler.mock.calls[0][0];
|
|
425
|
-
expect(cb.channelType).toBe(
|
|
426
|
-
expect(cb.chatId).toBe(
|
|
427
|
-
expect(cb.userId).toBe(
|
|
448
|
+
expect(cb.channelType).toBe("telegram");
|
|
449
|
+
expect(cb.chatId).toBe("12345");
|
|
450
|
+
expect(cb.userId).toBe("67890");
|
|
428
451
|
expect(cb.messageId).toBe(777);
|
|
429
|
-
expect(cb.callbackData).toBe(
|
|
430
|
-
expect(cb.callbackQueryId).toBe(
|
|
452
|
+
expect(cb.callbackData).toBe("q:abc:o:1");
|
|
453
|
+
expect(cb.callbackQueryId).toBe("cbq-test");
|
|
431
454
|
});
|
|
432
|
-
it(
|
|
455
|
+
it("answers callbacks when no handler is registered", async ()=>{
|
|
433
456
|
await adapter.start();
|
|
434
457
|
const bot = getMockBot();
|
|
435
|
-
const ctx = await bot._triggerCallback(12345, 67890,
|
|
458
|
+
const ctx = await bot._triggerCallback(12345, 67890, "q:x:o:0");
|
|
436
459
|
expect(ctx.answerCbQuery).toHaveBeenCalled();
|
|
437
460
|
});
|
|
438
|
-
it(
|
|
439
|
-
adapter.onCallback(vi.fn().mockRejectedValue(new Error(
|
|
461
|
+
it("answers with error when handler rejects", async ()=>{
|
|
462
|
+
adapter.onCallback(vi.fn().mockRejectedValue(new Error("boom")));
|
|
440
463
|
await adapter.start();
|
|
441
464
|
const bot = getMockBot();
|
|
442
|
-
const ctx = await bot._triggerCallback(12345, 67890,
|
|
443
|
-
expect(ctx.answerCbQuery).toHaveBeenCalledWith(
|
|
465
|
+
const ctx = await bot._triggerCallback(12345, 67890, "q:x:o:0");
|
|
466
|
+
expect(ctx.answerCbQuery).toHaveBeenCalledWith("Error");
|
|
444
467
|
});
|
|
445
468
|
});
|
|
446
|
-
describe(
|
|
447
|
-
it(
|
|
469
|
+
describe("isHealthy", ()=>{
|
|
470
|
+
it("should return true after start", async ()=>{
|
|
448
471
|
await adapter.start();
|
|
449
472
|
expect(await adapter.isHealthy()).toBe(true);
|
|
450
473
|
});
|
|
451
|
-
it(
|
|
474
|
+
it("should return false before start", async ()=>{
|
|
452
475
|
expect(await adapter.isHealthy()).toBe(false);
|
|
453
476
|
});
|
|
454
|
-
it(
|
|
477
|
+
it("should return false after stop", async ()=>{
|
|
455
478
|
await adapter.start();
|
|
456
479
|
await adapter.stop();
|
|
457
480
|
expect(await adapter.isHealthy()).toBe(false);
|