talon-agent 1.0.0 → 1.2.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/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
|
@@ -18,8 +18,12 @@ function createMockDeps() {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const context: ContextManager = {
|
|
21
|
-
acquire: vi.fn((chatId: number) => {
|
|
22
|
-
|
|
21
|
+
acquire: vi.fn((chatId: number) => {
|
|
22
|
+
acquired.push(chatId);
|
|
23
|
+
}),
|
|
24
|
+
release: vi.fn((chatId: number) => {
|
|
25
|
+
released.push(chatId);
|
|
26
|
+
}),
|
|
23
27
|
getMessageCount: vi.fn(() => 0),
|
|
24
28
|
};
|
|
25
29
|
|
|
@@ -168,13 +172,36 @@ describe("dispatcher", () => {
|
|
|
168
172
|
const deps = createMockDeps();
|
|
169
173
|
let resolveQuery!: () => void;
|
|
170
174
|
(deps.backend.query as ReturnType<typeof vi.fn>).mockImplementation(
|
|
171
|
-
() =>
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
() =>
|
|
176
|
+
new Promise<{
|
|
177
|
+
text: string;
|
|
178
|
+
durationMs: number;
|
|
179
|
+
inputTokens: number;
|
|
180
|
+
outputTokens: number;
|
|
181
|
+
cacheRead: number;
|
|
182
|
+
cacheWrite: number;
|
|
183
|
+
}>((r) => {
|
|
184
|
+
resolveQuery = () =>
|
|
185
|
+
r({
|
|
186
|
+
text: "",
|
|
187
|
+
durationMs: 0,
|
|
188
|
+
inputTokens: 0,
|
|
189
|
+
outputTokens: 0,
|
|
190
|
+
cacheRead: 0,
|
|
191
|
+
cacheWrite: 0,
|
|
192
|
+
});
|
|
193
|
+
}),
|
|
174
194
|
);
|
|
175
195
|
initDispatcher(deps);
|
|
176
196
|
|
|
177
|
-
const p = execute({
|
|
197
|
+
const p = execute({
|
|
198
|
+
chatId: "555",
|
|
199
|
+
numericChatId: 555,
|
|
200
|
+
prompt: "hi",
|
|
201
|
+
senderName: "U",
|
|
202
|
+
isGroup: false,
|
|
203
|
+
source: "message",
|
|
204
|
+
});
|
|
178
205
|
// Give it a tick to start
|
|
179
206
|
await new Promise((r) => setTimeout(r, 10));
|
|
180
207
|
expect(getActiveCount()).toBe(1);
|
|
@@ -191,21 +218,46 @@ describe("dispatcher", () => {
|
|
|
191
218
|
order.push(`start:${params.chatId}`);
|
|
192
219
|
await new Promise((r) => setTimeout(r, 50));
|
|
193
220
|
order.push(`end:${params.chatId}`);
|
|
194
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
text: "",
|
|
223
|
+
durationMs: 50,
|
|
224
|
+
inputTokens: 0,
|
|
225
|
+
outputTokens: 0,
|
|
226
|
+
cacheRead: 0,
|
|
227
|
+
cacheWrite: 0,
|
|
228
|
+
};
|
|
195
229
|
}),
|
|
196
230
|
};
|
|
197
231
|
|
|
198
232
|
initDispatcher({
|
|
199
233
|
backend,
|
|
200
|
-
context: {
|
|
234
|
+
context: {
|
|
235
|
+
acquire: () => {},
|
|
236
|
+
release: () => {},
|
|
237
|
+
getMessageCount: () => 0,
|
|
238
|
+
},
|
|
201
239
|
sendTyping: async () => {},
|
|
202
240
|
onActivity: () => {},
|
|
203
241
|
});
|
|
204
242
|
|
|
205
243
|
// Fire two queries for DIFFERENT chats — they should overlap
|
|
206
244
|
await Promise.all([
|
|
207
|
-
execute({
|
|
208
|
-
|
|
245
|
+
execute({
|
|
246
|
+
chatId: "A",
|
|
247
|
+
numericChatId: 1,
|
|
248
|
+
prompt: "a",
|
|
249
|
+
senderName: "U",
|
|
250
|
+
isGroup: false,
|
|
251
|
+
source: "message",
|
|
252
|
+
}),
|
|
253
|
+
execute({
|
|
254
|
+
chatId: "B",
|
|
255
|
+
numericChatId: 2,
|
|
256
|
+
prompt: "b",
|
|
257
|
+
senderName: "U",
|
|
258
|
+
isGroup: false,
|
|
259
|
+
source: "message",
|
|
260
|
+
}),
|
|
209
261
|
]);
|
|
210
262
|
|
|
211
263
|
// Both should START before either ENDS (true parallel)
|
|
@@ -219,19 +271,44 @@ describe("dispatcher", () => {
|
|
|
219
271
|
query: vi.fn(async () => {
|
|
220
272
|
callCount++;
|
|
221
273
|
if (callCount === 1) throw new Error("first fails");
|
|
222
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
text: "second ok",
|
|
276
|
+
durationMs: 10,
|
|
277
|
+
inputTokens: 0,
|
|
278
|
+
outputTokens: 0,
|
|
279
|
+
cacheRead: 0,
|
|
280
|
+
cacheWrite: 0,
|
|
281
|
+
};
|
|
223
282
|
}),
|
|
224
283
|
};
|
|
225
284
|
|
|
226
285
|
initDispatcher({
|
|
227
286
|
backend,
|
|
228
|
-
context: {
|
|
287
|
+
context: {
|
|
288
|
+
acquire: () => {},
|
|
289
|
+
release: () => {},
|
|
290
|
+
getMessageCount: () => 0,
|
|
291
|
+
},
|
|
229
292
|
sendTyping: async () => {},
|
|
230
293
|
onActivity: () => {},
|
|
231
294
|
});
|
|
232
295
|
|
|
233
|
-
const p1 = execute({
|
|
234
|
-
|
|
296
|
+
const p1 = execute({
|
|
297
|
+
chatId: "ERR",
|
|
298
|
+
numericChatId: 1,
|
|
299
|
+
prompt: "fail",
|
|
300
|
+
senderName: "U",
|
|
301
|
+
isGroup: false,
|
|
302
|
+
source: "message",
|
|
303
|
+
});
|
|
304
|
+
const p2 = execute({
|
|
305
|
+
chatId: "ERR",
|
|
306
|
+
numericChatId: 1,
|
|
307
|
+
prompt: "succeed",
|
|
308
|
+
senderName: "U",
|
|
309
|
+
isGroup: false,
|
|
310
|
+
source: "message",
|
|
311
|
+
});
|
|
235
312
|
|
|
236
313
|
await expect(p1).rejects.toThrow("first fails");
|
|
237
314
|
const result = await p2;
|
|
@@ -240,18 +317,31 @@ describe("dispatcher", () => {
|
|
|
240
317
|
|
|
241
318
|
it("activeCount is accurate during errors", async () => {
|
|
242
319
|
const backend: QueryBackend = {
|
|
243
|
-
query: vi.fn(async () => {
|
|
320
|
+
query: vi.fn(async () => {
|
|
321
|
+
throw new Error("boom");
|
|
322
|
+
}),
|
|
244
323
|
};
|
|
245
324
|
|
|
246
325
|
initDispatcher({
|
|
247
326
|
backend,
|
|
248
|
-
context: {
|
|
327
|
+
context: {
|
|
328
|
+
acquire: () => {},
|
|
329
|
+
release: () => {},
|
|
330
|
+
getMessageCount: () => 0,
|
|
331
|
+
},
|
|
249
332
|
sendTyping: async () => {},
|
|
250
333
|
onActivity: () => {},
|
|
251
334
|
});
|
|
252
335
|
|
|
253
336
|
await expect(
|
|
254
|
-
execute({
|
|
337
|
+
execute({
|
|
338
|
+
chatId: "X",
|
|
339
|
+
numericChatId: 1,
|
|
340
|
+
prompt: "x",
|
|
341
|
+
senderName: "U",
|
|
342
|
+
isGroup: false,
|
|
343
|
+
source: "message",
|
|
344
|
+
}),
|
|
255
345
|
).rejects.toThrow("boom");
|
|
256
346
|
|
|
257
347
|
expect(getActiveCount()).toBe(0); // cleaned up even on error
|
|
@@ -271,7 +361,11 @@ describe("dispatcher", () => {
|
|
|
271
361
|
|
|
272
362
|
initDispatcher({
|
|
273
363
|
backend,
|
|
274
|
-
context: {
|
|
364
|
+
context: {
|
|
365
|
+
acquire: () => {},
|
|
366
|
+
release: () => {},
|
|
367
|
+
getMessageCount: () => 0,
|
|
368
|
+
},
|
|
275
369
|
sendTyping: async () => {},
|
|
276
370
|
onActivity: () => {},
|
|
277
371
|
});
|
|
@@ -360,24 +454,331 @@ describe("dispatcher", () => {
|
|
|
360
454
|
order.push(`start:${params.text}`);
|
|
361
455
|
await new Promise((r) => setTimeout(r, 30));
|
|
362
456
|
order.push(`end:${params.text}`);
|
|
363
|
-
return {
|
|
457
|
+
return {
|
|
458
|
+
text: "",
|
|
459
|
+
durationMs: 30,
|
|
460
|
+
inputTokens: 0,
|
|
461
|
+
outputTokens: 0,
|
|
462
|
+
cacheRead: 0,
|
|
463
|
+
cacheWrite: 0,
|
|
464
|
+
};
|
|
364
465
|
}),
|
|
365
466
|
};
|
|
366
467
|
|
|
367
468
|
initDispatcher({
|
|
368
469
|
backend,
|
|
369
|
-
context: {
|
|
470
|
+
context: {
|
|
471
|
+
acquire: () => {},
|
|
472
|
+
release: () => {},
|
|
473
|
+
getMessageCount: () => 0,
|
|
474
|
+
},
|
|
370
475
|
sendTyping: async () => {},
|
|
371
476
|
onActivity: () => {},
|
|
372
477
|
});
|
|
373
478
|
|
|
374
479
|
// Fire two queries for the SAME chat — second must wait
|
|
375
480
|
await Promise.all([
|
|
376
|
-
execute({
|
|
377
|
-
|
|
481
|
+
execute({
|
|
482
|
+
chatId: "X",
|
|
483
|
+
numericChatId: 1,
|
|
484
|
+
prompt: "first",
|
|
485
|
+
senderName: "U",
|
|
486
|
+
isGroup: false,
|
|
487
|
+
source: "message",
|
|
488
|
+
}),
|
|
489
|
+
execute({
|
|
490
|
+
chatId: "X",
|
|
491
|
+
numericChatId: 1,
|
|
492
|
+
prompt: "second",
|
|
493
|
+
senderName: "U",
|
|
494
|
+
isGroup: false,
|
|
495
|
+
source: "message",
|
|
496
|
+
}),
|
|
378
497
|
]);
|
|
379
498
|
|
|
380
499
|
// Same chat: first completes before second starts
|
|
381
|
-
expect(order).toEqual([
|
|
500
|
+
expect(order).toEqual([
|
|
501
|
+
"start:first",
|
|
502
|
+
"end:first",
|
|
503
|
+
"start:second",
|
|
504
|
+
"end:second",
|
|
505
|
+
]);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe("typing indicator — interval error handling", () => {
|
|
510
|
+
it("logs warning when sendTyping interval callback rejects", async () => {
|
|
511
|
+
vi.useFakeTimers();
|
|
512
|
+
vi.resetModules();
|
|
513
|
+
vi.doMock("../util/log.js", () => ({
|
|
514
|
+
log: vi.fn(),
|
|
515
|
+
logDebug: vi.fn(),
|
|
516
|
+
logWarn: vi.fn(),
|
|
517
|
+
logError: vi.fn(),
|
|
518
|
+
}));
|
|
519
|
+
vi.doMock("../core/dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
520
|
+
|
|
521
|
+
const { initDispatcher, execute } = await import("../core/dispatcher.js");
|
|
522
|
+
const { logWarn } = (await import("../util/log.js")) as unknown as {
|
|
523
|
+
logWarn: ReturnType<typeof vi.fn>;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
let typingCallCount = 0;
|
|
527
|
+
let resolveQuery!: (v: {
|
|
528
|
+
text: string;
|
|
529
|
+
durationMs: number;
|
|
530
|
+
inputTokens: number;
|
|
531
|
+
outputTokens: number;
|
|
532
|
+
cacheRead: number;
|
|
533
|
+
cacheWrite: number;
|
|
534
|
+
}) => void;
|
|
535
|
+
|
|
536
|
+
initDispatcher({
|
|
537
|
+
backend: {
|
|
538
|
+
query: vi.fn(
|
|
539
|
+
() =>
|
|
540
|
+
new Promise((r) => {
|
|
541
|
+
resolveQuery = r;
|
|
542
|
+
}),
|
|
543
|
+
) as never,
|
|
544
|
+
},
|
|
545
|
+
context: { acquire: vi.fn(), release: vi.fn(), getMessageCount: () => 0 },
|
|
546
|
+
sendTyping: vi.fn(async () => {
|
|
547
|
+
typingCallCount++;
|
|
548
|
+
if (typingCallCount > 1) throw new Error("interval typing API error");
|
|
549
|
+
}),
|
|
550
|
+
onActivity: vi.fn(),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const p = execute({
|
|
554
|
+
chatId: "interval-err",
|
|
555
|
+
numericChatId: 888,
|
|
556
|
+
prompt: "test",
|
|
557
|
+
senderName: "U",
|
|
558
|
+
isGroup: false,
|
|
559
|
+
source: "message",
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Let the initial sendTyping call run, then trigger the 4000ms interval
|
|
563
|
+
await vi.advanceTimersByTimeAsync(4100);
|
|
564
|
+
|
|
565
|
+
resolveQuery({
|
|
566
|
+
text: "ok",
|
|
567
|
+
durationMs: 10,
|
|
568
|
+
inputTokens: 0,
|
|
569
|
+
outputTokens: 0,
|
|
570
|
+
cacheRead: 0,
|
|
571
|
+
cacheWrite: 0,
|
|
572
|
+
});
|
|
573
|
+
await p;
|
|
574
|
+
|
|
575
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
576
|
+
"dispatcher",
|
|
577
|
+
expect.stringContaining("interval failed"),
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
vi.useRealTimers();
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe("typing indicator — error handling", () => {
|
|
585
|
+
it("logs warning when sendTyping rejects (initial call)", async () => {
|
|
586
|
+
vi.resetModules();
|
|
587
|
+
vi.doMock("../util/log.js", () => ({
|
|
588
|
+
log: vi.fn(),
|
|
589
|
+
logDebug: vi.fn(),
|
|
590
|
+
logWarn: vi.fn(),
|
|
591
|
+
logError: vi.fn(),
|
|
592
|
+
}));
|
|
593
|
+
vi.doMock("./dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
594
|
+
vi.doMock("../core/dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
595
|
+
|
|
596
|
+
const { initDispatcher, execute } = await import("../core/dispatcher.js");
|
|
597
|
+
const logWarn = (await import("../util/log.js")).logWarn as ReturnType<
|
|
598
|
+
typeof vi.fn
|
|
599
|
+
>;
|
|
600
|
+
|
|
601
|
+
const backend = {
|
|
602
|
+
query: vi.fn(async () => ({
|
|
603
|
+
text: "ok",
|
|
604
|
+
durationMs: 10,
|
|
605
|
+
inputTokens: 0,
|
|
606
|
+
outputTokens: 0,
|
|
607
|
+
cacheRead: 0,
|
|
608
|
+
cacheWrite: 0,
|
|
609
|
+
})),
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
initDispatcher({
|
|
613
|
+
backend,
|
|
614
|
+
context: { acquire: vi.fn(), release: vi.fn(), getMessageCount: () => 0 },
|
|
615
|
+
sendTyping: vi.fn(async () => {
|
|
616
|
+
throw new Error("typing API error");
|
|
617
|
+
}),
|
|
618
|
+
onActivity: vi.fn(),
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await execute({
|
|
622
|
+
chatId: "typing-err-chat",
|
|
623
|
+
numericChatId: 999,
|
|
624
|
+
prompt: "test",
|
|
625
|
+
senderName: "User",
|
|
626
|
+
isGroup: false,
|
|
627
|
+
source: "message",
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
631
|
+
"dispatcher",
|
|
632
|
+
expect.stringContaining("sendTyping failed"),
|
|
633
|
+
);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("typing indicator — non-Error throws", () => {
|
|
638
|
+
it("logs warning with String(err) when sendTyping throws a non-Error (initial call)", async () => {
|
|
639
|
+
vi.resetModules();
|
|
640
|
+
vi.doMock("../util/log.js", () => ({
|
|
641
|
+
log: vi.fn(),
|
|
642
|
+
logDebug: vi.fn(),
|
|
643
|
+
logWarn: vi.fn(),
|
|
644
|
+
logError: vi.fn(),
|
|
645
|
+
}));
|
|
646
|
+
vi.doMock("../core/dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
647
|
+
|
|
648
|
+
const { initDispatcher, execute } = await import("../core/dispatcher.js");
|
|
649
|
+
const logWarn = (await import("../util/log.js")).logWarn as ReturnType<
|
|
650
|
+
typeof vi.fn
|
|
651
|
+
>;
|
|
652
|
+
|
|
653
|
+
initDispatcher({
|
|
654
|
+
backend: {
|
|
655
|
+
query: vi.fn(async () => ({
|
|
656
|
+
text: "ok",
|
|
657
|
+
durationMs: 10,
|
|
658
|
+
inputTokens: 0,
|
|
659
|
+
outputTokens: 0,
|
|
660
|
+
cacheRead: 0,
|
|
661
|
+
cacheWrite: 0,
|
|
662
|
+
})),
|
|
663
|
+
},
|
|
664
|
+
context: { acquire: vi.fn(), release: vi.fn(), getMessageCount: () => 0 },
|
|
665
|
+
// Throw a plain string (non-Error) to hit the `String(err)` branch at line 99
|
|
666
|
+
sendTyping: vi.fn(async () => {
|
|
667
|
+
throw "plain string typing error";
|
|
668
|
+
}), // eslint-disable-line @typescript-eslint/no-throw-literal
|
|
669
|
+
onActivity: vi.fn(),
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await execute({
|
|
673
|
+
chatId: "typing-non-error-chat",
|
|
674
|
+
numericChatId: 1001,
|
|
675
|
+
prompt: "test",
|
|
676
|
+
senderName: "User",
|
|
677
|
+
isGroup: false,
|
|
678
|
+
source: "message",
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
682
|
+
"dispatcher",
|
|
683
|
+
expect.stringContaining("plain string typing error"),
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("logs warning with String(err) when sendTyping interval throws a non-Error", async () => {
|
|
688
|
+
vi.useFakeTimers();
|
|
689
|
+
vi.resetModules();
|
|
690
|
+
vi.doMock("../util/log.js", () => ({
|
|
691
|
+
log: vi.fn(),
|
|
692
|
+
logDebug: vi.fn(),
|
|
693
|
+
logWarn: vi.fn(),
|
|
694
|
+
logError: vi.fn(),
|
|
695
|
+
}));
|
|
696
|
+
vi.doMock("../core/dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
697
|
+
|
|
698
|
+
const { initDispatcher, execute } = await import("../core/dispatcher.js");
|
|
699
|
+
const logWarn = (await import("../util/log.js")).logWarn as ReturnType<
|
|
700
|
+
typeof vi.fn
|
|
701
|
+
>;
|
|
702
|
+
|
|
703
|
+
let callCount = 0;
|
|
704
|
+
let resolveQuery!: (v: {
|
|
705
|
+
text: string;
|
|
706
|
+
durationMs: number;
|
|
707
|
+
inputTokens: number;
|
|
708
|
+
outputTokens: number;
|
|
709
|
+
cacheRead: number;
|
|
710
|
+
cacheWrite: number;
|
|
711
|
+
}) => void;
|
|
712
|
+
|
|
713
|
+
initDispatcher({
|
|
714
|
+
backend: {
|
|
715
|
+
query: vi.fn(
|
|
716
|
+
() =>
|
|
717
|
+
new Promise((r) => {
|
|
718
|
+
resolveQuery = r;
|
|
719
|
+
}),
|
|
720
|
+
) as never,
|
|
721
|
+
},
|
|
722
|
+
context: { acquire: vi.fn(), release: vi.fn(), getMessageCount: () => 0 },
|
|
723
|
+
// First call OK, subsequent calls throw a non-Error string (covers line 103 String(err) branch)
|
|
724
|
+
sendTyping: vi.fn(async () => {
|
|
725
|
+
callCount++;
|
|
726
|
+
if (callCount > 1) throw "non-error interval typing failure"; // eslint-disable-line @typescript-eslint/no-throw-literal
|
|
727
|
+
}),
|
|
728
|
+
onActivity: vi.fn(),
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const p = execute({
|
|
732
|
+
chatId: "interval-non-error-chat",
|
|
733
|
+
numericChatId: 1002,
|
|
734
|
+
prompt: "test",
|
|
735
|
+
senderName: "User",
|
|
736
|
+
isGroup: false,
|
|
737
|
+
source: "message",
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
await vi.advanceTimersByTimeAsync(4100);
|
|
741
|
+
resolveQuery({
|
|
742
|
+
text: "ok",
|
|
743
|
+
durationMs: 10,
|
|
744
|
+
inputTokens: 0,
|
|
745
|
+
outputTokens: 0,
|
|
746
|
+
cacheRead: 0,
|
|
747
|
+
cacheWrite: 0,
|
|
748
|
+
});
|
|
749
|
+
await p;
|
|
750
|
+
|
|
751
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
752
|
+
"dispatcher",
|
|
753
|
+
expect.stringContaining("interval failed"),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
vi.useRealTimers();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe("dispatcher — uninitialized guard", () => {
|
|
761
|
+
it("throws when execute is called before initDispatcher", async () => {
|
|
762
|
+
vi.resetModules();
|
|
763
|
+
vi.doMock("../util/log.js", () => ({
|
|
764
|
+
log: vi.fn(),
|
|
765
|
+
logDebug: vi.fn(),
|
|
766
|
+
logWarn: vi.fn(),
|
|
767
|
+
logError: vi.fn(),
|
|
768
|
+
}));
|
|
769
|
+
vi.doMock("../core/dream.js", () => ({ maybeStartDream: vi.fn() }));
|
|
770
|
+
|
|
771
|
+
const { execute } = await import("../core/dispatcher.js");
|
|
772
|
+
// deps is null because initDispatcher was never called in this fresh module
|
|
773
|
+
await expect(
|
|
774
|
+
execute({
|
|
775
|
+
chatId: "x",
|
|
776
|
+
numericChatId: 1,
|
|
777
|
+
prompt: "hi",
|
|
778
|
+
senderName: "U",
|
|
779
|
+
isGroup: false,
|
|
780
|
+
source: "message",
|
|
781
|
+
}),
|
|
782
|
+
).rejects.toThrow("Dispatcher not initialized");
|
|
382
783
|
});
|
|
383
784
|
});
|