@bytesbrains/pi-telegram-bridge 1.0.0 → 1.0.2
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/package.json +1 -1
- package/src/__tests__/helpers.test.ts +783 -0
- package/src/__tests__/index.test.ts +559 -0
- package/src/__tests__/tools.test.ts +251 -0
- package/src/helpers.ts +22 -5
- package/src/index.ts +6 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-telegram-bridge — Extension Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full extension: tool registration, session lifecycle,
|
|
5
|
+
* tool execution with mocked Telegram API.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
8
|
+
|
|
9
|
+
const TEST_TOKEN = "111:test";
|
|
10
|
+
const TEST_CHAT_ID = "999";
|
|
11
|
+
|
|
12
|
+
// ── Mock Extension API ───────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function createMockPI() {
|
|
15
|
+
const tools: Record<string, any> = {};
|
|
16
|
+
const listeners: Record<string, Array<(...args: any[]) => void>> = {};
|
|
17
|
+
const entries: Array<{ type: string; customType?: string; data: unknown }> = [];
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
registerTool: vi.fn((tool: any) => {
|
|
21
|
+
tools[tool.name] = tool;
|
|
22
|
+
}),
|
|
23
|
+
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
|
|
24
|
+
if (!listeners[event]) listeners[event] = [];
|
|
25
|
+
listeners[event].push(handler);
|
|
26
|
+
}),
|
|
27
|
+
sendUserMessage: vi.fn(),
|
|
28
|
+
appendEntry: vi.fn((customType: string, data: unknown) => {
|
|
29
|
+
entries.push({ type: "custom", customType, data });
|
|
30
|
+
}),
|
|
31
|
+
// Helpers for tests
|
|
32
|
+
_tools: tools,
|
|
33
|
+
_listeners: listeners,
|
|
34
|
+
_entries: entries,
|
|
35
|
+
_fireEvent(event: string, ...args: any[]) {
|
|
36
|
+
for (const h of listeners[event] || []) {
|
|
37
|
+
h(...args);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
_getTool(name: string) {
|
|
41
|
+
return tools[name];
|
|
42
|
+
},
|
|
43
|
+
_reset() {
|
|
44
|
+
Object.keys(tools).forEach((k) => delete tools[k]);
|
|
45
|
+
Object.keys(listeners).forEach((k) => delete listeners[k]);
|
|
46
|
+
entries.length = 0;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// We need to mock the helpers module. We'll import the extension
|
|
52
|
+
// but intercept its helper calls via module mocking.
|
|
53
|
+
let mockTelegramApi: any;
|
|
54
|
+
let mockSendMsg: any;
|
|
55
|
+
let mockPollReply: any;
|
|
56
|
+
let mockGetBotId: any;
|
|
57
|
+
let mockSeedLastUpdateId: any;
|
|
58
|
+
let mockPollUpdates: any;
|
|
59
|
+
|
|
60
|
+
vi.mock("../helpers", () => ({
|
|
61
|
+
getToken: () => {
|
|
62
|
+
if (!process.env.TELEGRAM_BOT_TOKEN) throw new Error("TELEGRAM_BOT_TOKEN not set");
|
|
63
|
+
return process.env.TELEGRAM_BOT_TOKEN;
|
|
64
|
+
},
|
|
65
|
+
getChatId: () => {
|
|
66
|
+
if (!process.env.TELEGRAM_CHAT_ID) throw new Error("TELEGRAM_CHAT_ID not set");
|
|
67
|
+
return process.env.TELEGRAM_CHAT_ID;
|
|
68
|
+
},
|
|
69
|
+
telegramApi: (...args: any[]) => mockTelegramApi(...args),
|
|
70
|
+
sendMsg: (...args: any[]) => mockSendMsg(...args),
|
|
71
|
+
pollReply: (...args: any[]) => mockPollReply(...args),
|
|
72
|
+
getBotId: () => mockGetBotId(),
|
|
73
|
+
seedLastUpdateId: (...args: any[]) => mockSeedLastUpdateId(...args),
|
|
74
|
+
pollUpdates: (...args: any[]) => mockPollUpdates(...args),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
// Import after mocks
|
|
78
|
+
import telegramBridge from "../index";
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.stubEnv("TELEGRAM_BOT_TOKEN", TEST_TOKEN);
|
|
82
|
+
vi.stubEnv("TELEGRAM_CHAT_ID", TEST_CHAT_ID);
|
|
83
|
+
|
|
84
|
+
mockTelegramApi = vi.fn().mockResolvedValue({ ok: true, result: { id: 123, username: "testbot" } });
|
|
85
|
+
mockSendMsg = vi.fn().mockResolvedValue(42);
|
|
86
|
+
mockPollReply = vi.fn().mockResolvedValue("Yes");
|
|
87
|
+
mockGetBotId = vi.fn().mockResolvedValue(123);
|
|
88
|
+
mockSeedLastUpdateId = vi.fn().mockImplementation((id: number) => Promise.resolve(id));
|
|
89
|
+
mockPollUpdates = vi.fn(); // fire and forget, no-op
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.unstubAllEnvs();
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Tool Registration ─────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe("extension registration", () => {
|
|
100
|
+
it("registers all 5 tools", () => {
|
|
101
|
+
const pi = createMockPI();
|
|
102
|
+
telegramBridge(pi as any);
|
|
103
|
+
|
|
104
|
+
expect(pi.registerTool).toHaveBeenCalledTimes(5);
|
|
105
|
+
expect(pi._getTool("telegram_listen")).toBeDefined();
|
|
106
|
+
expect(pi._getTool("telegram_send")).toBeDefined();
|
|
107
|
+
expect(pi._getTool("telegram_ask")).toBeDefined();
|
|
108
|
+
expect(pi._getTool("telegram_override")).toBeDefined();
|
|
109
|
+
expect(pi._getTool("telegram_status")).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("registers session_start and session_shutdown handlers", () => {
|
|
113
|
+
const pi = createMockPI();
|
|
114
|
+
telegramBridge(pi as any);
|
|
115
|
+
|
|
116
|
+
expect(pi.on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
117
|
+
expect(pi.on).toHaveBeenCalledWith("session_shutdown", expect.any(Function));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── telegram_listen Tool ──────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("telegram_listen tool", () => {
|
|
124
|
+
it("returns no messages when update queue is empty", async () => {
|
|
125
|
+
const pi = createMockPI();
|
|
126
|
+
telegramBridge(pi as any);
|
|
127
|
+
|
|
128
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
129
|
+
ok: true,
|
|
130
|
+
json: async () => ({ ok: true, result: [] }),
|
|
131
|
+
});
|
|
132
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
133
|
+
|
|
134
|
+
const tool = pi._getTool("telegram_listen");
|
|
135
|
+
const result = await tool.execute();
|
|
136
|
+
|
|
137
|
+
expect(result.content[0].text).toBe("No new messages.");
|
|
138
|
+
expect(result.isError).toBeFalsy();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("detects new text messages and returns them", async () => {
|
|
142
|
+
const pi = createMockPI();
|
|
143
|
+
telegramBridge(pi as any);
|
|
144
|
+
|
|
145
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
146
|
+
ok: true,
|
|
147
|
+
json: async () => ({
|
|
148
|
+
ok: true,
|
|
149
|
+
result: [
|
|
150
|
+
{
|
|
151
|
+
update_id: 42,
|
|
152
|
+
message: {
|
|
153
|
+
message_id: 1,
|
|
154
|
+
chat: { id: Number(TEST_CHAT_ID) },
|
|
155
|
+
from: { id: 999, is_bot: false },
|
|
156
|
+
text: "Hello from Telegram!",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
163
|
+
|
|
164
|
+
const tool = pi._getTool("telegram_listen");
|
|
165
|
+
const result = await tool.execute();
|
|
166
|
+
|
|
167
|
+
expect(result.content[0].text).toContain("Hello from Telegram!");
|
|
168
|
+
expect(result.details.message).toBe("Hello from Telegram!");
|
|
169
|
+
// Should have persisted the update id
|
|
170
|
+
expect(pi.appendEntry).toHaveBeenCalledWith("tg-last-update", {
|
|
171
|
+
update_id: 42,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns error when Telegram is unreachable", async () => {
|
|
176
|
+
const pi = createMockPI();
|
|
177
|
+
telegramBridge(pi as any);
|
|
178
|
+
|
|
179
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 500,
|
|
182
|
+
});
|
|
183
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
184
|
+
|
|
185
|
+
const tool = pi._getTool("telegram_listen");
|
|
186
|
+
const result = await tool.execute();
|
|
187
|
+
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content[0].text).toBe("Could not reach Telegram.");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns error on network failure", async () => {
|
|
193
|
+
const pi = createMockPI();
|
|
194
|
+
telegramBridge(pi as any);
|
|
195
|
+
|
|
196
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")));
|
|
197
|
+
|
|
198
|
+
const tool = pi._getTool("telegram_listen");
|
|
199
|
+
const result = await tool.execute();
|
|
200
|
+
|
|
201
|
+
expect(result.isError).toBe(true);
|
|
202
|
+
expect(result.content[0].text).toContain("ECONNREFUSED");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("skips callback_query updates", async () => {
|
|
206
|
+
const pi = createMockPI();
|
|
207
|
+
telegramBridge(pi as any);
|
|
208
|
+
|
|
209
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
210
|
+
ok: true,
|
|
211
|
+
json: async () => ({
|
|
212
|
+
ok: true,
|
|
213
|
+
result: [
|
|
214
|
+
{
|
|
215
|
+
update_id: 10,
|
|
216
|
+
callback_query: { id: "cb-1", data: "yes" },
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
222
|
+
|
|
223
|
+
const tool = pi._getTool("telegram_listen");
|
|
224
|
+
const result = await tool.execute();
|
|
225
|
+
|
|
226
|
+
expect(result.content[0].text).toBe("No new messages.");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── telegram_send Tool ────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
describe("telegram_send tool", () => {
|
|
233
|
+
it("sends a message and returns success", async () => {
|
|
234
|
+
const pi = createMockPI();
|
|
235
|
+
telegramBridge(pi as any);
|
|
236
|
+
|
|
237
|
+
const tool = pi._getTool("telegram_send");
|
|
238
|
+
const result = await tool.execute("id-1", { message: "Test message" });
|
|
239
|
+
|
|
240
|
+
expect(mockSendMsg).toHaveBeenCalledWith("Test message");
|
|
241
|
+
expect(result.content[0].text).toContain("Sent");
|
|
242
|
+
expect(result.isError).toBeFalsy();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("returns error when sending fails", async () => {
|
|
246
|
+
const pi = createMockPI();
|
|
247
|
+
mockSendMsg = vi.fn().mockRejectedValue(new Error("Chat not found"));
|
|
248
|
+
telegramBridge(pi as any);
|
|
249
|
+
|
|
250
|
+
const tool = pi._getTool("telegram_send");
|
|
251
|
+
const result = await tool.execute("id-1", { message: "Test" });
|
|
252
|
+
|
|
253
|
+
expect(result.isError).toBe(true);
|
|
254
|
+
expect(result.content[0].text).toContain("Chat not found");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── telegram_ask Tool ─────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe("telegram_ask tool", () => {
|
|
261
|
+
it("sends question and returns reply", async () => {
|
|
262
|
+
const pi = createMockPI();
|
|
263
|
+
mockPollReply = vi.fn().mockResolvedValue("No");
|
|
264
|
+
telegramBridge(pi as any);
|
|
265
|
+
|
|
266
|
+
const tool = pi._getTool("telegram_ask");
|
|
267
|
+
const result = await tool.execute("id-1", { question: "Deploy?" });
|
|
268
|
+
|
|
269
|
+
expect(mockSendMsg).toHaveBeenCalled();
|
|
270
|
+
const sendCall = mockSendMsg.mock.calls[0];
|
|
271
|
+
expect(sendCall[0]).toContain("Deploy?");
|
|
272
|
+
// Should have inline keyboard markup
|
|
273
|
+
expect(sendCall[1]).toBeDefined();
|
|
274
|
+
expect(sendCall[1].inline_keyboard).toHaveLength(3); // Yes, No, Explain
|
|
275
|
+
|
|
276
|
+
expect(result.content[0].text).toContain("No");
|
|
277
|
+
expect(result.details.answer).toBe("No");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("uses custom options for keyboard", async () => {
|
|
281
|
+
const pi = createMockPI();
|
|
282
|
+
mockPollReply = vi.fn().mockResolvedValue("Approve");
|
|
283
|
+
telegramBridge(pi as any);
|
|
284
|
+
|
|
285
|
+
const tool = pi._getTool("telegram_ask");
|
|
286
|
+
await tool.execute("id-1", {
|
|
287
|
+
question: "Merge?",
|
|
288
|
+
options: ["Approve", "Reject", "Request changes"],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const sendCall = mockSendMsg.mock.calls[0];
|
|
292
|
+
expect(sendCall[1].inline_keyboard).toEqual([
|
|
293
|
+
[{ text: "Approve", callback_data: "Approve" }],
|
|
294
|
+
[{ text: "Reject", callback_data: "Reject" }],
|
|
295
|
+
[{ text: "Request changes", callback_data: "Request changes" }],
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("returns timeout when no reply", async () => {
|
|
300
|
+
const pi = createMockPI();
|
|
301
|
+
mockPollReply = vi.fn().mockResolvedValue(null);
|
|
302
|
+
telegramBridge(pi as any);
|
|
303
|
+
|
|
304
|
+
const tool = pi._getTool("telegram_ask");
|
|
305
|
+
const result = await tool.execute("id-1", {
|
|
306
|
+
question: "Hello?",
|
|
307
|
+
timeoutMinutes: 1,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result.details.timedOut).toBe(true);
|
|
311
|
+
expect(result.content[0].text).toContain("Timeout");
|
|
312
|
+
// Should send a timeout notification
|
|
313
|
+
expect(mockSendMsg).toHaveBeenCalledTimes(2); // question + timeout msg
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("handles API errors", async () => {
|
|
317
|
+
const pi = createMockPI();
|
|
318
|
+
mockSendMsg = vi.fn().mockRejectedValue(new Error("API down"));
|
|
319
|
+
telegramBridge(pi as any);
|
|
320
|
+
|
|
321
|
+
const tool = pi._getTool("telegram_ask");
|
|
322
|
+
const result = await tool.execute("id-1", { question: "Test" });
|
|
323
|
+
|
|
324
|
+
expect(result.isError).toBe(true);
|
|
325
|
+
expect(result.content[0].text).toContain("API down");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── telegram_override Tool ────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
describe("telegram_override tool", () => {
|
|
332
|
+
it("formats override message with command and reason", async () => {
|
|
333
|
+
const pi = createMockPI();
|
|
334
|
+
mockPollReply = vi.fn().mockResolvedValue("Yes, proceed");
|
|
335
|
+
telegramBridge(pi as any);
|
|
336
|
+
|
|
337
|
+
const tool = pi._getTool("telegram_override");
|
|
338
|
+
const result = await tool.execute("id-1", {
|
|
339
|
+
command: "git push --force",
|
|
340
|
+
reason: "Force push detected",
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const sendCall = mockSendMsg.mock.calls[0];
|
|
344
|
+
expect(sendCall[0]).toContain("Supervisor blocked an action");
|
|
345
|
+
expect(sendCall[0]).toContain("git push --force");
|
|
346
|
+
expect(sendCall[0]).toContain("Force push detected");
|
|
347
|
+
expect(sendCall[1].inline_keyboard).toHaveLength(3);
|
|
348
|
+
expect(sendCall[1].inline_keyboard[0][0].text).toBe("Yes, proceed");
|
|
349
|
+
|
|
350
|
+
expect(result.details.action).toBe("proceed");
|
|
351
|
+
expect(result.details.choice).toBe("Yes, proceed");
|
|
352
|
+
expect(result.details.timedOut).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("returns abort for second option", async () => {
|
|
356
|
+
const pi = createMockPI();
|
|
357
|
+
mockPollReply = vi.fn().mockResolvedValue("No, cancel");
|
|
358
|
+
telegramBridge(pi as any);
|
|
359
|
+
|
|
360
|
+
const tool = pi._getTool("telegram_override");
|
|
361
|
+
const result = await tool.execute("id-1", {
|
|
362
|
+
command: "rm -rf /",
|
|
363
|
+
reason: "Dangerous",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(result.details.action).toBe("abort");
|
|
367
|
+
expect(result.details.choice).toBe("No, cancel");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("returns explain for custom third+ options", async () => {
|
|
371
|
+
const pi = createMockPI();
|
|
372
|
+
mockPollReply = vi.fn().mockResolvedValue("Give me details");
|
|
373
|
+
telegramBridge(pi as any);
|
|
374
|
+
|
|
375
|
+
const tool = pi._getTool("telegram_override");
|
|
376
|
+
const result = await tool.execute("id-1", {
|
|
377
|
+
command: "test",
|
|
378
|
+
reason: "test",
|
|
379
|
+
options: ["Go", "Stop", "Give me details", "Do it differently"],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(result.details.action).toBe("explain");
|
|
383
|
+
expect(result.details.choice).toBe("Give me details");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("includes optional context in message", async () => {
|
|
387
|
+
const pi = createMockPI();
|
|
388
|
+
mockPollReply = vi.fn().mockResolvedValue("Yes, proceed");
|
|
389
|
+
telegramBridge(pi as any);
|
|
390
|
+
|
|
391
|
+
const tool = pi._getTool("telegram_override");
|
|
392
|
+
await tool.execute("id-1", {
|
|
393
|
+
command: "test",
|
|
394
|
+
reason: "test",
|
|
395
|
+
context: "User asked for a clean rebuild",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const sendCall = mockSendMsg.mock.calls[0];
|
|
399
|
+
expect(sendCall[0]).toContain("User asked for a clean rebuild");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("returns timeout when no reply", async () => {
|
|
403
|
+
const pi = createMockPI();
|
|
404
|
+
mockPollReply = vi.fn().mockResolvedValue(null);
|
|
405
|
+
telegramBridge(pi as any);
|
|
406
|
+
|
|
407
|
+
const tool = pi._getTool("telegram_override");
|
|
408
|
+
const result = await tool.execute("id-1", {
|
|
409
|
+
command: "test",
|
|
410
|
+
reason: "test",
|
|
411
|
+
timeoutMinutes: 1,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(result.details.timedOut).toBe(true);
|
|
415
|
+
expect(result.details.action).toBe("abort");
|
|
416
|
+
expect(mockSendMsg).toHaveBeenCalledTimes(2); // question + timeout
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("handles errors gracefully", async () => {
|
|
420
|
+
const pi = createMockPI();
|
|
421
|
+
mockSendMsg = vi.fn().mockRejectedValue(new Error("Token invalid"));
|
|
422
|
+
telegramBridge(pi as any);
|
|
423
|
+
|
|
424
|
+
const tool = pi._getTool("telegram_override");
|
|
425
|
+
const result = await tool.execute("id-1", {
|
|
426
|
+
command: "test",
|
|
427
|
+
reason: "test",
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(result.isError).toBe(true);
|
|
431
|
+
expect(result.content[0].text).toContain("Token invalid");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("truncates long command in message", async () => {
|
|
435
|
+
const pi = createMockPI();
|
|
436
|
+
mockPollReply = vi.fn().mockResolvedValue("Yes, proceed");
|
|
437
|
+
telegramBridge(pi as any);
|
|
438
|
+
|
|
439
|
+
const longCommand = "x".repeat(500);
|
|
440
|
+
const tool = pi._getTool("telegram_override");
|
|
441
|
+
await tool.execute("id-1", {
|
|
442
|
+
command: longCommand,
|
|
443
|
+
reason: "test",
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const sendCall = mockSendMsg.mock.calls[0];
|
|
447
|
+
// Should truncate to 200 chars
|
|
448
|
+
expect(sendCall[0]).toContain("x".repeat(200));
|
|
449
|
+
expect(sendCall[0]).not.toContain("x".repeat(201));
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ── telegram_status Tool ──────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
describe("telegram_status tool", () => {
|
|
456
|
+
it("reports not configured when env vars missing", async () => {
|
|
457
|
+
vi.stubEnv("TELEGRAM_BOT_TOKEN", "");
|
|
458
|
+
vi.stubEnv("TELEGRAM_CHAT_ID", "");
|
|
459
|
+
const pi = createMockPI();
|
|
460
|
+
telegramBridge(pi as any);
|
|
461
|
+
|
|
462
|
+
const tool = pi._getTool("telegram_status");
|
|
463
|
+
const result = await tool.execute();
|
|
464
|
+
|
|
465
|
+
expect(result.content[0].text).toContain("Not configured");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("reports active when configured and reachable", async () => {
|
|
469
|
+
const pi = createMockPI();
|
|
470
|
+
mockTelegramApi = vi.fn().mockResolvedValue({
|
|
471
|
+
ok: true,
|
|
472
|
+
result: { username: "mycoolbot" },
|
|
473
|
+
});
|
|
474
|
+
telegramBridge(pi as any);
|
|
475
|
+
|
|
476
|
+
const tool = pi._getTool("telegram_status");
|
|
477
|
+
const result = await tool.execute();
|
|
478
|
+
|
|
479
|
+
expect(result.content[0].text).toContain("Active. Bot: @mycoolbot");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("reports unreachable when API fails", async () => {
|
|
483
|
+
const pi = createMockPI();
|
|
484
|
+
mockTelegramApi = vi.fn().mockRejectedValue(new Error("timeout"));
|
|
485
|
+
telegramBridge(pi as any);
|
|
486
|
+
|
|
487
|
+
const tool = pi._getTool("telegram_status");
|
|
488
|
+
const result = await tool.execute();
|
|
489
|
+
|
|
490
|
+
expect(result.content[0].text).toContain("Token set but unreachable");
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ── Session Lifecycle ─────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
describe("session lifecycle", () => {
|
|
497
|
+
it("starts listener on session_start", async () => {
|
|
498
|
+
const pi = createMockPI();
|
|
499
|
+
telegramBridge(pi as any);
|
|
500
|
+
|
|
501
|
+
const ctx = {
|
|
502
|
+
sessionManager: {
|
|
503
|
+
getEntries: () => [],
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Fire session_start
|
|
508
|
+
await pi._listeners["session_start"][0]({}, ctx);
|
|
509
|
+
|
|
510
|
+
// startListener() is fire-and-forget async — wait for it
|
|
511
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
512
|
+
|
|
513
|
+
// pollUpdates should have been called (fire and forget)
|
|
514
|
+
expect(mockPollUpdates).toHaveBeenCalledTimes(1);
|
|
515
|
+
expect(mockSeedLastUpdateId).toHaveBeenCalledWith(0, expect.any(AbortSignal));
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("restores lastUpdateId from session state", async () => {
|
|
519
|
+
const pi = createMockPI();
|
|
520
|
+
telegramBridge(pi as any);
|
|
521
|
+
|
|
522
|
+
// Fire session_start with existing state
|
|
523
|
+
const ctx = {
|
|
524
|
+
sessionManager: {
|
|
525
|
+
getEntries: () => [
|
|
526
|
+
{
|
|
527
|
+
type: "custom",
|
|
528
|
+
customType: "tg-last-update",
|
|
529
|
+
data: { update_id: 999 },
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
await pi._listeners["session_start"][0]({}, ctx);
|
|
536
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
537
|
+
|
|
538
|
+
// seedLastUpdateId should be called with 999
|
|
539
|
+
expect(mockSeedLastUpdateId).toHaveBeenCalledWith(999, expect.any(AbortSignal));
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("does not start listener twice on duplicate session_start", async () => {
|
|
543
|
+
const pi = createMockPI();
|
|
544
|
+
telegramBridge(pi as any);
|
|
545
|
+
|
|
546
|
+
const ctx = {
|
|
547
|
+
sessionManager: { getEntries: () => [] },
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
await pi._listeners["session_start"][0]({}, ctx);
|
|
551
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
552
|
+
expect(mockPollUpdates).toHaveBeenCalledTimes(1);
|
|
553
|
+
|
|
554
|
+
// Fire again — should not create another listener
|
|
555
|
+
await pi._listeners["session_start"][0]({}, ctx);
|
|
556
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
557
|
+
expect(mockPollUpdates).toHaveBeenCalledTimes(1);
|
|
558
|
+
});
|
|
559
|
+
});
|