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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +15 -11
  4. package/prompts/dream.md +7 -3
  5. package/prompts/heartbeat.md +30 -0
  6. package/prompts/identity.md +1 -0
  7. package/prompts/teams.md +3 -0
  8. package/prompts/telegram.md +1 -0
  9. package/src/__tests__/chat-settings.test.ts +108 -2
  10. package/src/__tests__/cleanup-registry.test.ts +58 -0
  11. package/src/__tests__/config.test.ts +118 -52
  12. package/src/__tests__/cron-store-extended.test.ts +661 -0
  13. package/src/__tests__/cron-store.test.ts +145 -11
  14. package/src/__tests__/daily-log.test.ts +224 -13
  15. package/src/__tests__/dispatcher.test.ts +424 -23
  16. package/src/__tests__/dream.test.ts +1028 -0
  17. package/src/__tests__/errors-extended.test.ts +428 -0
  18. package/src/__tests__/errors.test.ts +95 -3
  19. package/src/__tests__/fuzz.test.ts +87 -15
  20. package/src/__tests__/gateway-actions.test.ts +1174 -433
  21. package/src/__tests__/gateway-http.test.ts +210 -19
  22. package/src/__tests__/gateway-retry.test.ts +359 -0
  23. package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
  24. package/src/__tests__/graph.test.ts +830 -0
  25. package/src/__tests__/handlers-stream.test.ts +208 -0
  26. package/src/__tests__/handlers.test.ts +2539 -70
  27. package/src/__tests__/heartbeat.test.ts +364 -0
  28. package/src/__tests__/history-extended.test.ts +775 -0
  29. package/src/__tests__/history-persistence.test.ts +74 -19
  30. package/src/__tests__/history.test.ts +113 -79
  31. package/src/__tests__/integration.test.ts +43 -8
  32. package/src/__tests__/log-init.test.ts +129 -0
  33. package/src/__tests__/log.test.ts +23 -5
  34. package/src/__tests__/media-index.test.ts +317 -35
  35. package/src/__tests__/plugin.test.ts +314 -0
  36. package/src/__tests__/prompt-builder-extended.test.ts +296 -0
  37. package/src/__tests__/prompt-builder.test.ts +44 -9
  38. package/src/__tests__/sessions.test.ts +258 -4
  39. package/src/__tests__/storage-save-errors.test.ts +342 -0
  40. package/src/__tests__/teams-frontend.test.ts +526 -31
  41. package/src/__tests__/telegram-formatting.test.ts +82 -0
  42. package/src/__tests__/terminal-commands.test.ts +208 -1
  43. package/src/__tests__/terminal-renderer.test.ts +223 -0
  44. package/src/__tests__/time.test.ts +107 -0
  45. package/src/__tests__/workspace-migrate.test.ts +256 -0
  46. package/src/__tests__/workspace.test.ts +63 -1
  47. package/src/backend/claude-sdk/tools.ts +64 -18
  48. package/src/bootstrap.ts +14 -14
  49. package/src/cli.ts +440 -125
  50. package/src/core/cron.ts +20 -5
  51. package/src/core/dispatcher.ts +27 -9
  52. package/src/core/dream.ts +79 -24
  53. package/src/core/errors.ts +12 -2
  54. package/src/core/gateway-actions.ts +182 -46
  55. package/src/core/gateway.ts +93 -41
  56. package/src/core/heartbeat.ts +515 -0
  57. package/src/core/plugin.ts +1 -1
  58. package/src/core/prompt-builder.ts +1 -4
  59. package/src/core/pulse.ts +4 -3
  60. package/src/frontend/teams/actions.ts +3 -1
  61. package/src/frontend/teams/formatting.ts +47 -8
  62. package/src/frontend/teams/graph.ts +35 -11
  63. package/src/frontend/teams/index.ts +155 -57
  64. package/src/frontend/teams/tools.ts +4 -6
  65. package/src/frontend/telegram/actions.ts +358 -82
  66. package/src/frontend/telegram/admin.ts +162 -72
  67. package/src/frontend/telegram/callbacks.ts +16 -10
  68. package/src/frontend/telegram/commands.ts +37 -21
  69. package/src/frontend/telegram/formatting.ts +2 -4
  70. package/src/frontend/telegram/handlers.ts +262 -66
  71. package/src/frontend/telegram/index.ts +39 -14
  72. package/src/frontend/telegram/middleware.ts +14 -4
  73. package/src/frontend/telegram/userbot.ts +16 -4
  74. package/src/frontend/terminal/renderer.ts +1 -4
  75. package/src/index.ts +28 -4
  76. package/src/storage/chat-settings.ts +32 -9
  77. package/src/storage/cron-store.ts +53 -11
  78. package/src/storage/daily-log.ts +72 -19
  79. package/src/storage/history.ts +39 -21
  80. package/src/storage/media-index.ts +37 -12
  81. package/src/storage/sessions.ts +3 -2
  82. package/src/util/cleanup-registry.ts +34 -0
  83. package/src/util/config.ts +85 -23
  84. package/src/util/log.ts +47 -17
  85. package/src/util/paths.ts +10 -0
  86. package/src/util/time.ts +29 -6
  87. package/src/util/watchdog.ts +5 -1
  88. 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) => { acquired.push(chatId); }),
22
- release: vi.fn((chatId: number) => { released.push(chatId); }),
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
- () => new Promise<{ text: string; durationMs: number; inputTokens: number; outputTokens: number; cacheRead: number; cacheWrite: number }>((r) => {
172
- resolveQuery = () => r({ text: "", durationMs: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0 });
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({ chatId: "555", numericChatId: 555, prompt: "hi", senderName: "U", isGroup: false, source: "message" });
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 { text: "", durationMs: 50, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0 };
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: { acquire: () => {}, release: () => {}, getMessageCount: () => 0 },
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({ chatId: "A", numericChatId: 1, prompt: "a", senderName: "U", isGroup: false, source: "message" }),
208
- execute({ chatId: "B", numericChatId: 2, prompt: "b", senderName: "U", isGroup: false, source: "message" }),
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 { text: "second ok", durationMs: 10, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0 };
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: { acquire: () => {}, release: () => {}, getMessageCount: () => 0 },
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({ chatId: "ERR", numericChatId: 1, prompt: "fail", senderName: "U", isGroup: false, source: "message" });
234
- const p2 = execute({ chatId: "ERR", numericChatId: 1, prompt: "succeed", senderName: "U", isGroup: false, source: "message" });
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 () => { throw new Error("boom"); }),
320
+ query: vi.fn(async () => {
321
+ throw new Error("boom");
322
+ }),
244
323
  };
245
324
 
246
325
  initDispatcher({
247
326
  backend,
248
- context: { acquire: () => {}, release: () => {}, getMessageCount: () => 0 },
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({ chatId: "X", numericChatId: 1, prompt: "x", senderName: "U", isGroup: false, source: "message" }),
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: { acquire: () => {}, release: () => {}, getMessageCount: () => 0 },
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 { text: "", durationMs: 30, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0 };
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: { acquire: () => {}, release: () => {}, getMessageCount: () => 0 },
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({ chatId: "X", numericChatId: 1, prompt: "first", senderName: "U", isGroup: false, source: "message" }),
377
- execute({ chatId: "X", numericChatId: 1, prompt: "second", senderName: "U", isGroup: false, source: "message" }),
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(["start:first", "end:first", "start:second", "end:second"]);
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
  });