arisa 5.1.49 → 5.1.64

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 (56) hide show
  1. package/AGENTS.md +0 -2
  2. package/README.md +9 -0
  3. package/package.json +1 -1
  4. package/src/core/agent/agent-manager.js +49 -489
  5. package/src/core/agent/agent-session-lifecycle.js +181 -0
  6. package/src/core/agent/pi-capability-tools.js +183 -0
  7. package/src/core/artifacts/artifact-store.js +73 -17
  8. package/src/core/capabilities/capability-service.js +340 -0
  9. package/src/core/config/config-defaults.js +28 -1
  10. package/src/core/tasks/task-routing.js +7 -0
  11. package/src/core/tasks/task-runner.js +68 -0
  12. package/src/core/tasks/task-store.js +382 -92
  13. package/src/core/tools/tool-output-materializer.js +5 -5
  14. package/src/core/tools/tool-registry.js +20 -5
  15. package/src/core/tools/weighted-resource-governor.js +153 -0
  16. package/src/index.js +20 -0
  17. package/src/official-tools.lock.json +62 -45
  18. package/src/runtime/arisa-capabilities.js +51 -242
  19. package/src/runtime/create-app.js +11 -2
  20. package/src/runtime/create-headless-app.js +7 -4
  21. package/src/runtime/paths.js +4 -0
  22. package/src/runtime/service-manager.js +3 -1
  23. package/src/runtime/service-supervisor.js +98 -0
  24. package/src/transport/telegram/bot.js +186 -374
  25. package/src/transport/telegram/chat-queue.js +83 -6
  26. package/src/transport/telegram/prompt-builders.js +9 -0
  27. package/src/transport/telegram/reply-topic-routing.js +111 -0
  28. package/src/transport/telegram/task-dispatcher.js +96 -36
  29. package/src/transport/telegram/telegram-auth-controller.js +180 -0
  30. package/src/transport/telegram/telegram-session-bridge.js +177 -0
  31. package/src/transport/telegram/telegram-tools-command.js +28 -0
  32. package/src/transport/telegram/telegram-workspace-controller.js +66 -0
  33. package/src/transport/telegram/workspace-topic-store.js +228 -0
  34. package/test/agent-session-lifecycle.test.js +58 -0
  35. package/test/artifact-store.test.js +38 -2
  36. package/test/capabilities-security.test.js +58 -0
  37. package/test/chat-queue.test.js +32 -0
  38. package/test/context-and-task-bounds.test.js +76 -1
  39. package/test/device-code-message.test.js +9 -0
  40. package/test/media-caption.test.js +1 -1
  41. package/test/model-selection.test.js +9 -1
  42. package/test/official-tool-dependencies.test.js +1 -1
  43. package/test/paths.test.js +8 -0
  44. package/test/pi-capability-tools.test.js +65 -0
  45. package/test/service-manager.test.js +48 -0
  46. package/test/session-start-operational-notes.test.js +1 -1
  47. package/test/task-idempotency.test.js +40 -0
  48. package/test/task-routing.test.js +62 -0
  49. package/test/task-store.test.js +231 -7
  50. package/test/telegram-reply-topic-routing.test.js +94 -0
  51. package/test/telegram-task-dispatcher.test.js +150 -23
  52. package/test/telegram-text-artifact.test.js +13 -2
  53. package/test/telegram-tools-command.test.js +47 -0
  54. package/test/telegram-workspace-topic-store.test.js +124 -0
  55. package/test/tool-registry-run.test.js +41 -0
  56. package/test/weighted-resource-governor.test.js +95 -0
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createChatStateStore,
5
+ createPromptExecutionReceipt,
6
+ drainChatPromptQueue,
7
+ queueChatPrompt
8
+ } from "../src/transport/telegram/chat-queue.js";
9
+
10
+ test("execution receipts start only when their queued prompt begins", async () => {
11
+ const state = createChatStateStore().get("chat-1");
12
+ const starts = [];
13
+ const receipt = createPromptExecutionReceipt(() => starts.push("second"));
14
+ queueChatPrompt(state, "second", { receipt });
15
+ let releaseFirst;
16
+ const first = new Promise((resolve) => { releaseFirst = resolve; });
17
+
18
+ const draining = drainChatPromptQueue({
19
+ chatState: state,
20
+ initialPrompt: "first",
21
+ processPrompt: async ({ prompt }) => {
22
+ if (prompt === "first") await first;
23
+ }
24
+ });
25
+
26
+ await new Promise((resolve) => setImmediate(resolve));
27
+ assert.deepEqual(starts, []);
28
+ releaseFirst();
29
+ await draining;
30
+ assert.deepEqual(starts, ["second"]);
31
+ await receipt.promise;
32
+ });
@@ -4,6 +4,7 @@ import {
4
4
  collectText,
5
5
  ensureQueuedTelegramTyping,
6
6
  isSilentReply,
7
+ resolveIncomingBusyMessageMode,
7
8
  stopQueuedTelegramTyping
8
9
  } from "../src/transport/telegram/bot.js";
9
10
  import {
@@ -13,7 +14,7 @@ import {
13
14
  resolveTelegramBusyMessageMode,
14
15
  routeBusyPrompt
15
16
  } from "../src/transport/telegram/chat-queue.js";
16
- import { selectScheduledTasks } from "../src/core/agent/agent-manager.js";
17
+ import { selectScheduledTasks } from "../src/core/capabilities/capability-service.js";
17
18
 
18
19
  test("queued Telegram prompts start typing immediately and share one indicator", async () => {
19
20
  let actions = 0;
@@ -158,6 +159,8 @@ test("chat state uses one queue for numeric and string chat IDs", () => {
158
159
  processing: false,
159
160
  pendingPrompts: [],
160
161
  pendingPromptContexts: [],
162
+ pendingPromptReceipts: [],
163
+ pendingPromptCoalescible: [],
161
164
  continueAfterClose: false,
162
165
  historyRevision: 0,
163
166
  beforeNextPrompt: null,
@@ -183,6 +186,32 @@ test("queued prompts retain their message boundaries and order", async () => {
183
186
  assert.deepEqual(processed, ["first request", "second request", "third request"]);
184
187
  });
185
188
 
189
+ test("queued execution receipts resolve only after their prompt runs", async () => {
190
+ const chatState = createChatStateStore().get("chat");
191
+ chatState.processing = true;
192
+ let releaseFirst;
193
+ const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
194
+ const { createPromptExecutionReceipt } = await import("../src/transport/telegram/chat-queue.js");
195
+ const receipt = createPromptExecutionReceipt();
196
+ queueChatPrompt(chatState, "scheduled request", { receipt });
197
+ let confirmed = false;
198
+ receipt.promise.then(() => { confirmed = true; });
199
+
200
+ const draining = drainChatPromptQueue({
201
+ chatState,
202
+ initialPrompt: "active request",
203
+ processPrompt: async ({ prompt }) => {
204
+ if (prompt === "active request") await firstGate;
205
+ }
206
+ });
207
+ await new Promise((resolve) => setImmediate(resolve));
208
+ assert.equal(confirmed, false);
209
+ releaseFirst();
210
+ await receipt.promise;
211
+ assert.equal(confirmed, true);
212
+ await draining;
213
+ });
214
+
186
215
  test("a queued /new continues after the active session closes", async () => {
187
216
  const chatState = createChatStateStore().get("chat");
188
217
  chatState.processing = true;
@@ -265,6 +294,20 @@ test("busy message mode supports global defaults and per-chat overrides", () =>
265
294
  assert.equal(resolveTelegramBusyMessageMode({ telegram: { busyMessageMode: "invalid" } }, 123), "queue");
266
295
  });
267
296
 
297
+ test("forum topics queue while direct Arisa messages retain steer mode", () => {
298
+ const config = { telegram: { busyMessageMode: "steer", chatMeta: {} } };
299
+ assert.equal(resolveIncomingBusyMessageMode({
300
+ config,
301
+ route: { workspace: true, sessionId: "owner--topic-87" },
302
+ message: { text: "topic follow-up" }
303
+ }), "queue");
304
+ assert.equal(resolveIncomingBusyMessageMode({
305
+ config,
306
+ route: { workspace: false, sessionId: "879964957" },
307
+ message: { text: "direct follow-up" }
308
+ }), "steer");
309
+ });
310
+
268
311
  test("steer mode sends text to the active Pi session", async () => {
269
312
  const received = [];
270
313
  const chatState = createChatStateStore().get("chat");
@@ -297,6 +340,38 @@ test("failed or unavailable steering falls back to the ordered queue", async ()
297
340
  assert.deepEqual(chatState.pendingPrompts, ["keep this", "and this"]);
298
341
  });
299
342
 
343
+ test("direct steer fallback coalesces consecutive text without reordering it", async () => {
344
+ const steered = [];
345
+ const chatState = createChatStateStore().get("chat");
346
+
347
+ const unavailable = await routeBusyPrompt({
348
+ chatState,
349
+ prompt: "first direct message",
350
+ mode: "steer",
351
+ coalesceQueued: true,
352
+ ctx: { message: { message_id: 1 } }
353
+ });
354
+ chatState.activeSession = {
355
+ isStreaming: true,
356
+ async steer(prompt) { steered.push(prompt); }
357
+ };
358
+ const next = await routeBusyPrompt({
359
+ chatState,
360
+ prompt: "second direct message",
361
+ mode: "steer",
362
+ coalesceQueued: true,
363
+ ctx: { message: { message_id: 2 } }
364
+ });
365
+
366
+ assert.equal(unavailable.disposition, "queued");
367
+ assert.equal(next.disposition, "coalesced");
368
+ assert.deepEqual(steered, []);
369
+ assert.deepEqual(chatState.pendingPrompts, [
370
+ "first direct message\n\n--- next direct message ---\n\nsecond direct message"
371
+ ]);
372
+ assert.equal(chatState.pendingPromptContexts[0].message.message_id, 2);
373
+ });
374
+
300
375
  test("a pending /new forces later text into the replacement queue", async () => {
301
376
  const steered = [];
302
377
  const chatState = createChatStateStore().get("chat");
@@ -1,6 +1,15 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { buildDeviceCodeTelegramMessage } from "../src/transport/telegram/device-code-message.js";
4
+ import { selectTelegramLoginOption } from "../src/transport/telegram/telegram-auth-controller.js";
5
+
6
+ test("Telegram auth prefers device login and falls back deterministically", () => {
7
+ const browser = { id: "oauth", label: "Browser login" };
8
+ const device = { id: "device-code", label: "Device code" };
9
+ assert.strictEqual(selectTelegramLoginOption([browser, device]), device);
10
+ assert.strictEqual(selectTelegramLoginOption([browser]), browser);
11
+ assert.equal(selectTelegramLoginOption([]), null);
12
+ });
4
13
 
5
14
  test("builds a copyable device-code Telegram message with HTML and inline buttons", () => {
6
15
  const payload = buildDeviceCodeTelegramMessage({
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { resolveMediaCaption } from "../src/core/agent/agent-manager.js";
3
+ import { resolveMediaCaption } from "../src/core/capabilities/capability-service.js";
4
4
 
5
5
  test("does not turn a domain-like filename into a caption", () => {
6
6
  assert.equal(resolveMediaCaption(undefined), undefined);
@@ -10,7 +10,12 @@ import {
10
10
  selectChatSpeed,
11
11
  selectChatThinkingLevel
12
12
  } from "../src/core/agent/model-selection.js";
13
- import { applyConfigDefaults, piConfigDefaults, telegramConfigDefaults } from "../src/core/config/config-defaults.js";
13
+ import {
14
+ applyConfigDefaults,
15
+ piConfigDefaults,
16
+ telegramConfigDefaults,
17
+ toolExecutionConfigDefaults
18
+ } from "../src/core/config/config-defaults.js";
14
19
  import {
15
20
  clampModelThinkingLevel,
16
21
  listModelThinkingLevels,
@@ -295,6 +300,9 @@ test("centralizes Telegram and Pi defaults in config", () => {
295
300
 
296
301
  assert.equal(config.telegram.modelPickerPageSize, telegramConfigDefaults.modelPickerPageSize);
297
302
  assert.equal(config.telegram.busyMessageMode, "steer");
303
+ assert.equal(config.toolExecution.defaultCapacity, toolExecutionConfigDefaults.defaultCapacity);
304
+ assert.equal(config.toolExecution.maxQueuedPerClass, 100);
305
+ assert.deepEqual(config.toolExecution.capacities, {});
298
306
  assert.equal(config.pi.thinkingLevel, piConfigDefaults.thinkingLevel);
299
307
  assert.equal(config.pi.speed, piConfigDefaults.speed);
300
308
  });
@@ -7,7 +7,7 @@ async function manifest(name) {
7
7
  }
8
8
 
9
9
  test("official orchestrators declare their hard tool dependencies", async () => {
10
- assert.deepEqual((await manifest("magnific-mcp")).toolDependencies, { "mcp-client": "^0.1.0" });
10
+ assert.deepEqual((await manifest("magnific-mcp")).toolDependencies, { "mcp-client": "^0.2.0" });
11
11
  assert.deepEqual((await manifest("campaign-draft-runner")).toolDependencies, {
12
12
  "pr-campaign": "^0.1.0",
13
13
  "gmail-workspace": "^0.1.0"
@@ -10,6 +10,7 @@ import {
10
10
  createIpcSocketPath,
11
11
  getChatArtifactsDir,
12
12
  getChatSessionSeedFile,
13
+ getChatTelegramWorkspacesFile,
13
14
  getChatToolConfigPath,
14
15
  getChatToolUsageFile,
15
16
  getChatToolStateDir,
@@ -39,6 +40,13 @@ test("keeps tool usage scoped below the chat state directory", () => {
39
40
  );
40
41
  });
41
42
 
43
+ test("keeps Telegram workspace topics scoped below the owner chat state directory", () => {
44
+ assert.equal(
45
+ getChatTelegramWorkspacesFile("chat-1"),
46
+ path.join(chatsDir, "chat-1", "state", "telegram-workspaces.json")
47
+ );
48
+ });
49
+
42
50
  test("keeps chat tool state and config paths scoped below the chat directory for normal names", () => {
43
51
  assert.equal(
44
52
  getChatToolStateDir("chat-1", "strudel-agent"),
@@ -0,0 +1,65 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createPiCapabilityTools } from "../src/core/agent/pi-capability-tools.js";
4
+
5
+ function createHarness() {
6
+ const calls = [];
7
+ let taskContext = { transportChatId: "chat-1", messageThreadId: 10 };
8
+ const capabilityService = {
9
+ async execute(request) {
10
+ calls.push(request);
11
+ if (request.method === "tools.list") return { tools: [] };
12
+ if (request.method === "tools.run") return { ok: true, output: {} };
13
+ return { ok: true };
14
+ }
15
+ };
16
+ const telegram = {
17
+ getTaskContext: () => taskContext,
18
+ sendMedia: async () => {}
19
+ };
20
+ const tools = createPiCapabilityTools({
21
+ capabilityService,
22
+ telegram,
23
+ chatId: "session-1",
24
+ policy: {
25
+ workspaceDir: "/workspace",
26
+ tools: ["read"],
27
+ excludeTools: [],
28
+ shell: {}
29
+ }
30
+ });
31
+ return {
32
+ calls,
33
+ tools,
34
+ setTaskContext(value) {
35
+ taskContext = value;
36
+ }
37
+ };
38
+ }
39
+
40
+ test("Pi tools delegate capability policy to CapabilityService", async () => {
41
+ const harness = createHarness();
42
+ const listTools = harness.tools.find((tool) => tool.name === "list_tools");
43
+
44
+ await listTools.execute("call-1", { query: "audio" });
45
+
46
+ assert.equal(harness.calls.length, 1);
47
+ assert.equal(harness.calls[0].method, "tools.list");
48
+ assert.equal(harness.calls[0].actorToolName, "list_tools");
49
+ assert.equal(harness.calls[0].chatId, "session-1");
50
+ assert.equal(harness.calls[0].context.workspaceDir, "/workspace");
51
+ assert.equal(harness.calls[0].context.coreTools[0].name, "read");
52
+ assert.equal(harness.calls[0].context.nativeTools[0].name, "system_shell");
53
+ });
54
+
55
+ test("Pi tool execution resolves the current Telegram task context per call", async () => {
56
+ const harness = createHarness();
57
+ const runTool = harness.tools.find((tool) => tool.name === "run_tool");
58
+
59
+ await runTool.execute("call-1", { name: "worker", args: {} });
60
+ harness.setTaskContext({ transportChatId: "chat-1", messageThreadId: 20 });
61
+ await runTool.execute("call-2", { name: "worker", args: {} });
62
+
63
+ assert.equal(harness.calls[0].context.taskContext.messageThreadId, 10);
64
+ assert.equal(harness.calls[1].context.taskContext.messageThreadId, 20);
65
+ });
@@ -1,7 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import { EventEmitter } from "node:events";
3
4
  import { createTelegramRestartHandler, telegramCommands } from "../src/transport/telegram/bot.js";
4
5
  import { handoffServiceRestart, restartService, serviceEntryFile, waitForServiceStop } from "../src/runtime/service-manager.js";
6
+ import { createServiceSupervisor } from "../src/runtime/service-supervisor.js";
5
7
 
6
8
  test("registers maintenance as native Telegram commands", () => {
7
9
  assert.equal(
@@ -225,6 +227,52 @@ test("restart does not start a second service when shutdown times out", async ()
225
227
  assert.equal(started, false);
226
228
  });
227
229
 
230
+ test("accepts restart handoff from a worker owned by the active supervisor", async () => {
231
+ let spawned = false;
232
+ await handoffServiceRestart({}, {
233
+ ensureHome: async () => {},
234
+ getStatus: async () => ({ running: true, pid: 77 }),
235
+ openLog: async () => ({ fd: 17, close: async () => {} }),
236
+ spawnProcess: () => ({ pid: 84, unref() { spawned = true; } }),
237
+ environment: { ARISA_SUPERVISOR_PID: "77" },
238
+ currentPid: 41
239
+ });
240
+ assert.equal(spawned, true);
241
+ });
242
+
243
+ test("supervisor restarts an unexpectedly exited worker and forwards shutdown", async () => {
244
+ const children = [];
245
+ const delays = [];
246
+ const spawnProcess = () => {
247
+ const child = new EventEmitter();
248
+ child.pid = 100 + children.length;
249
+ child.kill = (signal) => {
250
+ child.killedWith = signal;
251
+ queueMicrotask(() => child.emit("exit", 0, signal));
252
+ };
253
+ children.push(child);
254
+ return child;
255
+ };
256
+ const supervisor = createServiceSupervisor({
257
+ command: "node",
258
+ args: ["worker.js"],
259
+ restartLimit: 2,
260
+ restartBackoffMs: 5,
261
+ restartBackoffMaxMs: 20,
262
+ stableRuntimeMs: 60_000,
263
+ spawnProcess,
264
+ wait: async (ms) => { delays.push(ms); }
265
+ });
266
+ const running = supervisor.start();
267
+ children[0].emit("exit", 1, null);
268
+ await new Promise((resolve) => setImmediate(resolve));
269
+ assert.equal(children.length, 2);
270
+ assert.deepEqual(delays, [5]);
271
+ await supervisor.stop();
272
+ await running;
273
+ assert.equal(children[1].killedWith, "SIGTERM");
274
+ });
275
+
228
276
  test("requires explicit positive shutdown timing policy", async () => {
229
277
  await assert.rejects(
230
278
  waitForServiceStop({ timeoutMs: 0, pollIntervalMs: 1 }),
@@ -16,7 +16,7 @@ async function loadNotesWithHome(homeDir, notesPayload) {
16
16
  );
17
17
 
18
18
  const script = `
19
- const mod = await import(${JSON.stringify(new URL("../src/core/agent/agent-manager.js", import.meta.url).href)});
19
+ const mod = await import(${JSON.stringify(new URL("../src/core/agent/agent-session-lifecycle.js", import.meta.url).href)});
20
20
  process.stdout.write(JSON.stringify(mod.loadSessionStartOperationalNotes()));
21
21
  `;
22
22
  const { stdout } = await execFileAsync(process.execPath, ["--input-type=module", "--eval", script], {
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), "arisa-task-idempotency-home-"));
8
+ process.env.HOME = homeDir;
9
+ process.env.USERPROFILE = homeDir;
10
+
11
+ const { TaskStore } = await import("../src/core/tasks/task-store.js");
12
+ const { createTaskRunner } = await import("../src/core/tasks/task-runner.js");
13
+ const { arisaHomeDir } = await import("../src/runtime/paths.js");
14
+
15
+ test("two concurrent dispatchers execute one task id exactly once", async () => {
16
+ await rm(arisaHomeDir, { recursive: true, force: true });
17
+ const seedStore = new TaskStore();
18
+ await seedStore.add({
19
+ id: "single-execution",
20
+ kind: "agent_task",
21
+ runAt: new Date(Date.now() - 1_000).toISOString()
22
+ });
23
+
24
+ const executions = [];
25
+ const dispatch = async (task) => {
26
+ executions.push(task.id);
27
+ await new Promise((resolve) => setTimeout(resolve, 20));
28
+ };
29
+ const first = createTaskRunner({ taskStore: new TaskStore(), dispatch });
30
+ const second = createTaskRunner({ taskStore: new TaskStore(), dispatch });
31
+
32
+ const results = (await Promise.all([
33
+ first.dispatchDueTasks(),
34
+ second.dispatchDueTasks()
35
+ ])).flat();
36
+
37
+ assert.deepEqual(executions, ["single-execution"]);
38
+ assert.deepEqual(results, [{ taskId: "single-execution", status: "completed" }]);
39
+ assert.equal((await new TaskStore().get("single-execution")).status, "done");
40
+ });
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { taskWithoutCallerRouting } from "../src/core/tasks/task-routing.js";
4
+ import { materializeToolOutput } from "../src/core/tools/tool-output-materializer.js";
5
+
6
+ test("external tasks cannot override owner scope or transport routing", () => {
7
+ assert.deepEqual(taskWithoutCallerRouting({
8
+ kind: "agent_task",
9
+ route: { transport: "telegram", destination: { chatId: -999, threadId: 1 } },
10
+ payload: {
11
+ chatId: "other-owner",
12
+ telegramContext: { transportChatId: -999, messageThreadId: 1 },
13
+ prompt: "safe"
14
+ }
15
+ }), {
16
+ kind: "agent_task",
17
+ payload: { prompt: "safe" }
18
+ });
19
+ });
20
+
21
+ test("tool output materialization stores routing outside task payloads", async () => {
22
+ let capturedTasks;
23
+ let capturedDefaults;
24
+ const taskStore = {
25
+ async addMany(tasks, defaults) {
26
+ capturedTasks = tasks;
27
+ capturedDefaults = defaults;
28
+ return tasks;
29
+ }
30
+ };
31
+ const artifactStore = {
32
+ forChat() {
33
+ return {
34
+ async createText() { throw new Error("unexpected text artifact"); },
35
+ async createFromFile() { throw new Error("unexpected file artifact"); }
36
+ };
37
+ }
38
+ };
39
+ const route = { transport: "telegram", destination: { chatId: -1001, threadId: 87 } };
40
+
41
+ await materializeToolOutput({
42
+ result: {
43
+ asyncTask: {
44
+ kind: "agent_task",
45
+ route: { transport: "telegram", destination: { chatId: -999, threadId: 1 } },
46
+ payload: { chatId: "other", prompt: "run" }
47
+ }
48
+ },
49
+ name: "scheduler",
50
+ chatId: "owner",
51
+ artifactStore,
52
+ taskStore,
53
+ taskContext: route
54
+ });
55
+
56
+ assert.deepEqual(capturedTasks, [{ kind: "agent_task", payload: { prompt: "run" } }]);
57
+ assert.deepEqual(capturedDefaults, {
58
+ payload: { chatId: "owner" },
59
+ route,
60
+ source: { type: "tool", toolName: "scheduler", chatId: "owner" }
61
+ });
62
+ });