shadow-claw 1.40.0 → 1.40.1

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 (86) hide show
  1. package/.well-known/mcp/server-card.json +2 -2
  2. package/.well-known/mcp/servers.json +1 -1
  3. package/.well-known/mcp.json +2 -2
  4. package/dist/cli/cli.js +2 -2
  5. package/dist/lib/components/index.js +2 -2
  6. package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
  7. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  8. package/dist/lib/components/shadow-claw.js +1 -1
  9. package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
  10. package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
  11. package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
  12. package/dist/lib/index.js +2 -2
  13. package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
  14. package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
  15. package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
  16. package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
  17. package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
  18. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
  19. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
  20. package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
  21. package/dist/public/.well-known/mcp/server-card.json +2 -2
  22. package/dist/public/.well-known/mcp/servers.json +1 -1
  23. package/dist/public/.well-known/mcp.json +2 -2
  24. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  25. package/dist/public/docs/example/article/index.html +2 -2
  26. package/dist/public/docs/publishing/index.html +2 -2
  27. package/dist/public/docs/skill-creator/index.html +2 -2
  28. package/dist/public/index.html +2 -2
  29. package/dist/public/index.js +1 -1
  30. package/dist/public/main/index.html +2 -2
  31. package/dist/public/main/memory/index.html +2 -2
  32. package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
  33. package/dist/public/service-worker.js +1 -1
  34. package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
  35. package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
  36. package/dist/server.js +1 -1
  37. package/package.json +1 -1
  38. package/src/cli/cli.ts +2 -2
  39. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
  40. package/src/core/effect.test.ts +19 -0
  41. package/src/core/orchestrator/orchestrator-state.ts +2 -1
  42. package/src/core/orchestrator/orchestrator.test.ts +686 -5
  43. package/src/core/orchestrator/orchestrator.ts +3 -10
  44. package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
  45. package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
  46. package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
  47. package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
  48. package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
  49. package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
  50. package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
  51. package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
  52. package/src/core/orchestrator/utils/initTasks.ts +5 -4
  53. package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
  54. package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
  55. package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
  56. package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
  57. package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
  58. package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
  59. package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
  60. package/src/core/orchestrator/utils/operations/task.ts +1 -1
  61. package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
  62. package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
  63. package/src/core/orchestrator/utils/settings.test.ts +248 -81
  64. package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
  65. package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
  66. package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
  67. package/src/server/routes/mcp.ts +2 -2
  68. package/src/stores/file-viewer.test.ts +11 -0
  69. package/src/stores/theme.test.ts +41 -2
  70. package/src/stores/toast.test.ts +25 -0
  71. package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
  72. package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
  73. package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
  74. package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
  75. package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
  76. package/src/worker/tools/node-transformers-executor.test.ts +105 -6
  77. package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
  78. package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
  79. package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
  80. package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
  81. package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
  82. package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
  83. package/src/worker/tools/tasks/update-task.test.ts +78 -14
  84. package/src/worker/tools/ui/javascript.test.ts +70 -17
  85. package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
  86. package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
@@ -1,45 +1,59 @@
1
1
  import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2
-
3
- const mockBuildDynamicContext = jest.fn() as any;
4
- const mockEstimateTokens = jest.fn() as any;
5
- const mockBuildConversationMessages = jest.fn() as any;
6
- const mockGetConfig = jest.fn() as any;
7
- const mockListGroups = jest.fn() as any;
8
- const mockSaveMessage = jest.fn() as any;
9
- const mockReadGroupFile = jest.fn() as any;
10
-
11
- const mockInvokeWithLiteRtLm = jest.fn() as any;
12
- const mockIsLiteRtLmSupported = jest.fn() as any;
13
-
14
- const mockInvokeWithPromptApi = jest.fn() as any;
15
- const mockIsPromptApiSupported = jest.fn() as any;
16
-
17
- const mockGetContextLimit = jest.fn() as any;
18
- const mockGetProvider = jest.fn() as any;
19
-
20
- const mockInvokeWithTransformersJs = jest.fn() as any;
21
- const mockUlid = jest.fn() as any;
22
- const mockWorkerPost = jest.fn() as any;
23
- const mockRegisterSubagentCollector = jest.fn() as any;
24
- const mockUnregisterSubagentCollector = jest.fn() as any;
25
- const mockBuildSystemPrompt = jest.fn() as any;
26
-
27
- const mockGetChannelTypeForGroup = jest.fn() as any;
2
+ import type { ShadowClawDatabase } from "../../../db/db.js";
3
+ import type { Orchestrator } from "../orchestrator.js";
4
+ import { activate_skill } from "../../../subsystems/skills/tool.js";
5
+
6
+ const mockBuildDynamicContext = jest.fn<(...args: unknown[]) => unknown>();
7
+ const mockEstimateTokens = jest.fn<(...args: unknown[]) => number>();
8
+ const mockBuildConversationMessages =
9
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
10
+ const mockGetConfig = jest.fn<(...args: unknown[]) => Promise<unknown>>();
11
+ const mockListGroups = jest.fn<(...args: unknown[]) => Promise<unknown>>();
12
+ const mockSaveMessage = jest.fn<(...args: unknown[]) => Promise<unknown>>();
13
+ const mockReadGroupFile = jest.fn<(...args: unknown[]) => Promise<unknown>>();
14
+
15
+ const mockInvokeWithLiteRtLm =
16
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
17
+ const mockIsLiteRtLmSupported = jest.fn<() => boolean>();
18
+
19
+ const mockInvokeWithPromptApi =
20
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
21
+ const mockIsPromptApiSupported = jest.fn<() => boolean>();
22
+ const mockEnsureBuiltinAiPolyfills = jest.fn<() => Promise<unknown>>();
23
+
24
+ const mockGetContextLimit = jest.fn<(...args: unknown[]) => number>();
25
+ const mockGetProvider = jest.fn<(...args: unknown[]) => unknown>();
26
+
27
+ const mockInvokeWithTransformersJs =
28
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
29
+ const mockUlid = jest.fn<() => string>();
30
+ const mockWorkerPost = jest.fn<(...args: unknown[]) => unknown>();
31
+ const mockRegisterSubagentCollector =
32
+ jest.fn<(...args: unknown[]) => unknown>();
33
+ const mockUnregisterSubagentCollector =
34
+ jest.fn<(...args: unknown[]) => unknown>();
35
+ const mockBuildSystemPrompt = jest.fn<(...args: unknown[]) => string>();
36
+
37
+ const mockGetChannelTypeForGroup = jest.fn<(...args: unknown[]) => unknown>();
28
38
  jest.unstable_mockModule("./operations/channel.js", () => ({
29
39
  getChannelTypeForGroup: mockGetChannelTypeForGroup,
30
40
  }));
31
41
 
32
- const mockLoadDeclarativeTools = jest.fn() as any;
33
- const mockFindDeclarativeTool = jest.fn() as any;
42
+ const mockLoadDeclarativeTools =
43
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
44
+ const mockFindDeclarativeTool = jest.fn<(...args: unknown[]) => unknown>();
34
45
  jest.unstable_mockModule("../../../subsystems/tools/declarative.js", () => ({
35
46
  loadDeclarativeTools: mockLoadDeclarativeTools,
36
47
  findDeclarativeTool: mockFindDeclarativeTool,
37
48
  }));
38
49
 
39
- const mockGetApiKeyForRequest = jest.fn() as any;
40
- const mockGetProviderRuntimeHeaders = jest.fn() as any;
41
- const mockGetReasoningConfig = jest.fn() as any;
42
- const mockStartTransformersProgressPolling = jest.fn() as any;
50
+ const mockGetApiKeyForRequest =
51
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
52
+ const mockGetProviderRuntimeHeaders =
53
+ jest.fn<(...args: unknown[]) => unknown>();
54
+ const mockGetReasoningConfig = jest.fn<(...args: unknown[]) => unknown>();
55
+ const mockStartTransformersProgressPolling =
56
+ jest.fn<(...args: unknown[]) => unknown>();
43
57
  jest.unstable_mockModule("./operations/provider.js", () => ({
44
58
  getApiKeyForRequest: mockGetApiKeyForRequest,
45
59
  getProviderRuntimeHeaders: mockGetProviderRuntimeHeaders,
@@ -47,23 +61,48 @@ jest.unstable_mockModule("./operations/provider.js", () => ({
47
61
  startTransformersProgressPolling: mockStartTransformersProgressPolling,
48
62
  }));
49
63
 
50
- const mockCompactContext = jest.fn() as any;
64
+ const mockCompactContext = jest.fn<(...args: unknown[]) => Promise<unknown>>();
51
65
  jest.unstable_mockModule("./compactContext.js", () => ({
52
66
  compactContext: mockCompactContext,
53
67
  }));
54
68
 
55
- const mockDeliverResponse = jest.fn() as any;
69
+ const mockDeliverResponse = jest.fn<(...args: unknown[]) => Promise<unknown>>();
56
70
  jest.unstable_mockModule("./deliverResponse.js", () => ({
57
71
  deliverResponse: mockDeliverResponse,
58
72
  }));
59
73
 
60
- const mockHandleWorkerMessage = jest.fn() as any;
74
+ const mockDispatchSubagentInvoke =
75
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
76
+ jest.unstable_mockModule("./dispatchSubagentInvoke.js", () => ({
77
+ dispatchSubagentInvoke: mockDispatchSubagentInvoke,
78
+ }));
79
+
80
+ const mockHandleWorkerMessage =
81
+ jest.fn<(...args: unknown[]) => Promise<unknown>>();
61
82
  jest.unstable_mockModule("./handleWorkerMessage.js", () => ({
62
83
  handleWorkerMessage: mockHandleWorkerMessage,
63
84
  }));
64
85
 
86
+ const mockDiscoverSkills = jest.fn<(...args: unknown[]) => Promise<unknown>>();
87
+ jest.unstable_mockModule(
88
+ "../../../subsystems/skills/discoverSkills.js",
89
+ () => ({
90
+ discoverSkills: mockDiscoverSkills,
91
+ }),
92
+ );
93
+
94
+ jest.unstable_mockModule(
95
+ "../../../subsystems/providers/builtin-ai-tasks.js",
96
+ () => ({
97
+ ensureBuiltinAiPolyfills: mockEnsureBuiltinAiPolyfills,
98
+ }),
99
+ );
100
+
65
101
  jest.unstable_mockModule("../../../config/config.js", () => ({
66
- CONFIG_KEYS: { STORAGE_HANDLE: "STORAGE_HANDLE" },
102
+ CONFIG_KEYS: {
103
+ STORAGE_HANDLE: "STORAGE_HANDLE",
104
+ PROMPT_API_FALLBACK_MODEL: "PROMPT_API_FALLBACK_MODEL",
105
+ },
67
106
  OPFS_ROOT: "shadowclaw",
68
107
  DEFAULT_GROUP_ID: "br:main",
69
108
  DEFAULT_MAX_ITERATIONS: 50,
@@ -121,7 +160,7 @@ jest.unstable_mockModule("../../../db/getConfig.js", () => ({
121
160
  }));
122
161
 
123
162
  jest.unstable_mockModule("../../../db/db.js", () => ({
124
- getDb: jest.fn<any>().mockResolvedValue(null),
163
+ getDb: jest.fn<() => Promise<null>>().mockResolvedValue(null),
125
164
  }));
126
165
 
127
166
  jest.unstable_mockModule("../../../db/groups.js", () => ({
@@ -138,25 +177,40 @@ jest.unstable_mockModule("../../../storage/readGroupFile.js", () => ({
138
177
  readGroupFile: mockReadGroupFile,
139
178
  }));
140
179
 
180
+ interface MockTokenUsage {
181
+ inputTokens?: number;
182
+ cacheReadTokens?: number;
183
+ outputTokens?: number;
184
+ }
185
+
186
+ const mockOrchestratorStore = {
187
+ getPeerState: jest.fn<() => Record<string, unknown> | undefined>(),
188
+ tokenUsage: null as MockTokenUsage | null,
189
+ };
190
+
141
191
  jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
142
- orchestratorStore: {
143
- getPeerState: jest.fn(),
144
- tokenUsage: null,
145
- },
192
+ orchestratorStore: mockOrchestratorStore,
146
193
  }));
147
194
 
148
- jest.unstable_mockModule("../../../stores/tools.js", () => ({
149
- toolsStore: {
150
- allTools: [{ name: "tool1" }],
151
- enabledTools: [{ name: "tool1" }],
152
- refreshDeclarativeTools: jest.fn(async (db: any, groupId: any) => {
153
- const res = await mockLoadDeclarativeTools(db, groupId);
195
+ const mockToolsStore = {
196
+ allTools: [{ name: "tool1" }],
197
+ enabledTools: [{ name: "tool1" }],
198
+ systemPromptOverride: undefined as string | undefined,
199
+ refreshDeclarativeTools: jest.fn(
200
+ async (db: ShadowClawDatabase, groupId: string) => {
201
+ const res = (await mockLoadDeclarativeTools(db, groupId)) as {
202
+ tools?: Array<{ name: string }>;
203
+ };
154
204
  return res?.tools || [];
155
- }),
156
- isDeclarativeToolEnabled: jest.fn(
157
- (name: string) => name !== "disabled_decl_tool",
158
- ),
159
- },
205
+ },
206
+ ),
207
+ isDeclarativeToolEnabled: jest.fn(
208
+ (name: string) => name !== "disabled_decl_tool",
209
+ ),
210
+ };
211
+
212
+ jest.unstable_mockModule("../../../stores/tools.js", () => ({
213
+ toolsStore: mockToolsStore,
160
214
  }));
161
215
 
162
216
  jest.unstable_mockModule(
@@ -208,17 +262,24 @@ jest.unstable_mockModule("../../../worker/utils/system-prompt.js", () => ({
208
262
  const { invokeAgent } = await import("./invokeAgent.js");
209
263
 
210
264
  describe("invokeAgent", () => {
211
- let mockOrchestrator: any;
212
- let mockDb: any;
265
+ let mockOrchestrator: Orchestrator;
266
+ let mockDb: ShadowClawDatabase;
213
267
 
214
268
  beforeEach(() => {
215
269
  jest.clearAllMocks();
216
- mockDb = {};
270
+ mockDb = {} as unknown as ShadowClawDatabase;
271
+ mockOrchestratorStore.tokenUsage = null;
272
+ mockOrchestratorStore.getPeerState.mockReturnValue(undefined);
273
+
274
+ mockToolsStore.allTools = [{ name: "tool1" }];
275
+ mockToolsStore.enabledTools = [{ name: "tool1" }];
276
+ mockToolsStore.systemPromptOverride = undefined;
277
+
217
278
  mockOrchestrator = {
218
- inFlightTriggerByGroup: new Map(),
219
- inFlightEffectiveProviderByGroup: new Map(),
220
- pendingScheduledTasks: new Set(),
221
- schedulerTriggeredGroups: new Set(),
279
+ inFlightTriggerByGroup: new Map<string, string>(),
280
+ inFlightEffectiveProviderByGroup: new Map<string, unknown>(),
281
+ pendingScheduledTasks: new Set<string>(),
282
+ schedulerTriggeredGroups: new Set<string>(),
222
283
  setState: jest.fn(),
223
284
  router: { setTyping: jest.fn() },
224
285
  events: { emit: jest.fn() },
@@ -229,14 +290,18 @@ describe("invokeAgent", () => {
229
290
  contextCompressionEnabled: false,
230
291
  maxTokens: 1000,
231
292
  maxIterations: 5,
232
- getApiKeyForSpecificProvider: (jest.fn() as any).mockResolvedValue("key"),
293
+ getApiKeyForSpecificProvider: jest
294
+ .fn<() => Promise<string>>()
295
+ .mockResolvedValue("key"),
233
296
  rateLimitAutoAdapt: false,
234
297
  rateLimitCallsPerMinute: 60,
235
298
  streamingEnabled: true,
236
- createProviderRequestId: jest.fn().mockReturnValue("req-123"),
299
+ createProviderRequestId: jest
300
+ .fn<() => string>()
301
+ .mockReturnValue("req-123"),
237
302
  agentWorker: { postMessage: jest.fn() },
238
- promptControllers: new Map(),
239
- };
303
+ promptControllers: new Map<string, AbortController>(),
304
+ } as unknown as Orchestrator;
240
305
 
241
306
  mockGetChannelTypeForGroup.mockReturnValue("web");
242
307
  mockGetApiKeyForRequest.mockResolvedValue("key");
@@ -244,7 +309,10 @@ describe("invokeAgent", () => {
244
309
  mockGetReasoningConfig.mockReturnValue({});
245
310
  mockCompactContext.mockResolvedValue(undefined);
246
311
  mockDeliverResponse.mockResolvedValue(undefined);
312
+ mockDispatchSubagentInvoke.mockResolvedValue(undefined);
247
313
  mockHandleWorkerMessage.mockResolvedValue(undefined);
314
+ mockDiscoverSkills.mockResolvedValue({ skills: [] });
315
+ mockEnsureBuiltinAiPolyfills.mockResolvedValue(undefined);
248
316
  mockStartTransformersProgressPolling.mockReturnValue(undefined);
249
317
 
250
318
  mockGetConfig.mockResolvedValue("storage-handle");
@@ -264,8 +332,8 @@ describe("invokeAgent", () => {
264
332
  tools: [],
265
333
  diagnostics: [],
266
334
  });
267
- mockGetProvider.mockImplementation((id: string) => ({
268
- defaultModel: "default-" + id,
335
+ mockGetProvider.mockImplementation((id: unknown) => ({
336
+ defaultModel: "default-" + String(id),
269
337
  supportsStreaming: true,
270
338
  format: "openai",
271
339
  }));
@@ -279,7 +347,7 @@ describe("invokeAgent", () => {
279
347
  "thinking",
280
348
  "group1",
281
349
  );
282
- expect(mockOrchestrator.router.setTyping).toHaveBeenCalledWith(
350
+ expect(mockOrchestrator.router?.setTyping).toHaveBeenCalledWith(
283
351
  "group1",
284
352
  true,
285
353
  );
@@ -289,6 +357,21 @@ describe("invokeAgent", () => {
289
357
  });
290
358
  });
291
359
 
360
+ it("should gracefully handle readGroupFile errors", async () => {
361
+ mockReadGroupFile.mockRejectedValue(new Error("File not found"));
362
+
363
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
364
+
365
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
366
+ expect.objectContaining({
367
+ type: "invoke",
368
+ payload: expect.objectContaining({
369
+ memory: "",
370
+ }),
371
+ }),
372
+ );
373
+ });
374
+
292
375
  it("should save scheduled task message", async () => {
293
376
  mockUlid.mockReturnValue("msg-id");
294
377
 
@@ -314,7 +397,7 @@ describe("invokeAgent", () => {
314
397
  );
315
398
  });
316
399
 
317
- it("should auto-compact context if usage is high", async () => {
400
+ it("should auto-compact context if usage is high and trigger compactContext", async () => {
318
401
  mockBuildConversationMessages.mockResolvedValue(new Array(15).fill({}));
319
402
  mockBuildDynamicContext.mockReturnValue({
320
403
  messages: [],
@@ -330,7 +413,6 @@ describe("invokeAgent", () => {
330
413
  expect.any(Object),
331
414
  );
332
415
 
333
- // Fast-forward microtasks
334
416
  await new Promise((resolve) => setTimeout(resolve, 0));
335
417
  expect(mockCompactContext).toHaveBeenCalledWith(
336
418
  mockOrchestrator,
@@ -340,10 +422,6 @@ describe("invokeAgent", () => {
340
422
  });
341
423
 
342
424
  it("should blend actual token usage to prevent meter regression", async () => {
343
- const { orchestratorStore } =
344
- await import("../../../stores/orchestrator.js");
345
-
346
- // Set a baseline heuristic that is low (e.g. 50 + 100 = 150)
347
425
  mockBuildDynamicContext.mockReturnValue({
348
426
  messages: [],
349
427
  estimatedTokens: 50,
@@ -351,8 +429,7 @@ describe("invokeAgent", () => {
351
429
  truncatedCount: 0,
352
430
  });
353
431
 
354
- // Mock the actual token usage from the API to be much higher (e.g. a huge cached prompt)
355
- (orchestratorStore as any).tokenUsage = {
432
+ mockOrchestratorStore.tokenUsage = {
356
433
  inputTokens: 100,
357
434
  cacheReadTokens: 3000,
358
435
  outputTokens: 50,
@@ -360,28 +437,179 @@ describe("invokeAgent", () => {
360
437
 
361
438
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
362
439
 
363
- // The emitted context usage should use the actual token footprint (100 + 3000 + 50 = 3150)
364
- // rather than falling back to the 150 heuristic
365
440
  expect(mockOrchestrator.events.emit).toHaveBeenCalledWith(
366
441
  "context-usage",
367
442
  expect.objectContaining({
368
443
  estimatedTokens: 3150,
369
444
  }),
370
445
  );
446
+ });
447
+
448
+ it("should handle token usage zero fallbacks when fields are undefined", async () => {
449
+ mockBuildDynamicContext.mockReturnValue({
450
+ messages: [],
451
+ estimatedTokens: 50,
452
+ usagePercent: 5,
453
+ truncatedCount: 0,
454
+ });
455
+
456
+ mockOrchestratorStore.tokenUsage = {};
457
+
458
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
459
+
460
+ expect(mockOrchestrator.events.emit).toHaveBeenCalledWith(
461
+ "context-usage",
462
+ expect.objectContaining({
463
+ estimatedTokens: 250,
464
+ }),
465
+ );
466
+ });
467
+
468
+ it("should pass peer state to buildSystemPrompt when available", async () => {
469
+ mockOrchestratorStore.getPeerState.mockReturnValue({ peerId: "node-1" });
371
470
 
372
- // Cleanup
373
- (orchestratorStore as any).tokenUsage = null;
471
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
472
+
473
+ expect(mockBuildSystemPrompt).toHaveBeenCalledWith(
474
+ "Assistant",
475
+ "memory content",
476
+ expect.any(Array),
477
+ undefined,
478
+ { peerId: "node-1" },
479
+ [],
480
+ { groupId: "group1" },
481
+ );
374
482
  });
375
483
 
376
- it("should handle transformers_js_browser", async () => {
484
+ it("should append activate_skill to activeTools when skills are discovered", async () => {
485
+ mockDiscoverSkills.mockResolvedValue({
486
+ skills: [{ name: "weather-skill" }],
487
+ });
488
+
489
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
490
+
491
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
492
+ expect.objectContaining({
493
+ payload: expect.objectContaining({
494
+ enabledTools: expect.arrayContaining([
495
+ expect.objectContaining({ name: "tool1" }),
496
+ activate_skill,
497
+ ]),
498
+ }),
499
+ }),
500
+ );
501
+ });
502
+
503
+ it("should not duplicate activate_skill if already in tools", async () => {
504
+ mockToolsStore.allTools = [{ name: activate_skill.name }];
505
+ mockToolsStore.enabledTools = [{ name: activate_skill.name }];
506
+ mockDiscoverSkills.mockResolvedValue({
507
+ skills: [{ name: "weather-skill" }],
508
+ });
509
+
510
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
511
+
512
+ const call = (
513
+ mockOrchestrator.agentWorker?.postMessage as unknown as jest.Mock
514
+ ).mock.calls[0][0] as {
515
+ payload: { enabledTools: Array<{ name: string }> };
516
+ };
517
+ const matching = call.payload.enabledTools.filter(
518
+ (t) => t.name === activate_skill.name,
519
+ );
520
+ expect(matching).toHaveLength(1);
521
+ });
522
+
523
+ it("should handle subagentModelSelectionMode manual", async () => {
524
+ mockListGroups.mockResolvedValue([
525
+ { groupId: "group1", subagentModelSelectionMode: "manual" },
526
+ ]);
527
+
528
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
529
+
530
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
531
+ expect.objectContaining({
532
+ payload: expect.objectContaining({
533
+ subagentModelSelectionMode: "manual",
534
+ }),
535
+ }),
536
+ );
537
+ });
538
+
539
+ it("should resolve prompt_api fallback model when prompt api is not supported", async () => {
540
+ mockListGroups.mockResolvedValue([
541
+ {
542
+ groupId: "group1",
543
+ pinnedProvider: "prompt_api",
544
+ pinnedModel: "browser-built-in",
545
+ },
546
+ ]);
547
+ mockIsPromptApiSupported.mockReturnValue(false);
548
+ mockGetConfig.mockImplementation(async (...args: unknown[]) => {
549
+ const key = args[1] as string;
550
+ if (key === "PROMPT_API_FALLBACK_MODEL") {
551
+ return "custom-fallback-model";
552
+ }
553
+ return "storage-handle";
554
+ });
555
+
556
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
557
+
558
+ expect(mockGetContextLimit).toHaveBeenCalledWith("custom-fallback-model");
559
+ });
560
+
561
+ it("should default prompt_api fallback model when config has no fallback", async () => {
562
+ mockListGroups.mockResolvedValue([
563
+ {
564
+ groupId: "group1",
565
+ pinnedProvider: "prompt_api",
566
+ pinnedModel: "",
567
+ },
568
+ ]);
569
+ mockIsPromptApiSupported.mockReturnValue(false);
570
+ mockGetConfig.mockImplementation(async (...args: unknown[]) => {
571
+ const key = args[1] as string;
572
+ if (key === "PROMPT_API_FALLBACK_MODEL") {
573
+ return "";
574
+ }
575
+ return "storage-handle";
576
+ });
577
+
578
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
579
+
580
+ expect(mockGetContextLimit).toHaveBeenCalledWith(
581
+ "onnx-community/Qwen3-0.6B-ONNX",
582
+ );
583
+ });
584
+
585
+ it("should handle transformers_js_browser lifecycle, subagent invocation, and worker message", async () => {
377
586
  mockListGroups.mockResolvedValue([
378
587
  { groupId: "group1", pinnedProvider: "transformers_js_browser" },
379
588
  ]);
380
- mockInvokeWithTransformersJs.mockResolvedValue(undefined);
589
+ mockInvokeWithTransformersJs.mockImplementation(
590
+ async (...args: unknown[]) => {
591
+ const onMessage = args[5] as (msg: unknown) => Promise<void>;
592
+ const ctx = args[9] as {
593
+ invokeSubagent: (payload: unknown) => Promise<void>;
594
+ };
595
+ await onMessage({ type: "chunk", data: "test" });
596
+ await ctx.invokeSubagent({ prompt: "spawn subagent" });
597
+ },
598
+ );
381
599
 
382
600
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
383
601
 
384
602
  expect(mockInvokeWithTransformersJs).toHaveBeenCalled();
603
+ expect(mockHandleWorkerMessage).toHaveBeenCalledWith(
604
+ mockOrchestrator,
605
+ mockDb,
606
+ { type: "chunk", data: "test" },
607
+ );
608
+ expect(mockDispatchSubagentInvoke).toHaveBeenCalledWith(
609
+ mockDb,
610
+ { prompt: "spawn subagent" },
611
+ expect.any(AbortSignal),
612
+ );
385
613
  expect(mockOrchestrator.promptControllers.has("group1")).toBe(false);
386
614
  });
387
615
 
@@ -396,9 +624,10 @@ describe("invokeAgent", () => {
396
624
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
397
625
 
398
626
  expect(mockDeliverResponse).not.toHaveBeenCalled();
627
+ expect(mockOrchestrator.promptControllers.has("group1")).toBe(false);
399
628
  });
400
629
 
401
- it("should handle transformers_js_browser error", async () => {
630
+ it("should handle transformers_js_browser error and non-error thrown", async () => {
402
631
  mockListGroups.mockResolvedValue([
403
632
  { groupId: "group1", pinnedProvider: "transformers_js_browser" },
404
633
  ]);
@@ -414,21 +643,49 @@ describe("invokeAgent", () => {
414
643
  "group1",
415
644
  expect.stringContaining("Transformers error"),
416
645
  );
646
+
647
+ mockInvokeWithTransformersJs.mockRejectedValue("string failure");
648
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
649
+
650
+ expect(mockDeliverResponse).toHaveBeenCalledWith(
651
+ mockOrchestrator,
652
+ mockDb,
653
+ "group1",
654
+ expect.stringContaining("string failure"),
655
+ );
417
656
  });
418
657
 
419
- it("should handle prompt_api", async () => {
658
+ it("should handle prompt_api lifecycle, subagent invocation, and worker message", async () => {
420
659
  mockListGroups.mockResolvedValue([
421
660
  { groupId: "group1", pinnedProvider: "prompt_api" },
422
661
  ]);
423
662
  mockIsPromptApiSupported.mockReturnValue(true);
424
- mockInvokeWithPromptApi.mockResolvedValue(undefined);
663
+ mockInvokeWithPromptApi.mockImplementation(async (...args: unknown[]) => {
664
+ const onMessage = args[5] as (msg: unknown) => Promise<void>;
665
+ const ctx = args[8] as {
666
+ invokeSubagent: (payload: unknown) => Promise<void>;
667
+ };
668
+ await onMessage({ type: "prompt-chunk" });
669
+ await ctx.invokeSubagent({ prompt: "prompt-sub" });
670
+ });
425
671
 
426
672
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
427
673
 
428
674
  expect(mockInvokeWithPromptApi).toHaveBeenCalled();
675
+ expect(mockHandleWorkerMessage).toHaveBeenCalledWith(
676
+ mockOrchestrator,
677
+ mockDb,
678
+ { type: "prompt-chunk" },
679
+ );
680
+ expect(mockDispatchSubagentInvoke).toHaveBeenCalledWith(
681
+ mockDb,
682
+ { prompt: "prompt-sub" },
683
+ expect.any(AbortSignal),
684
+ );
685
+ expect(mockOrchestrator.promptControllers.has("group1")).toBe(false);
429
686
  });
430
687
 
431
- it("should handle prompt_api not supported", async () => {
688
+ it("should polyfill and handle prompt_api unsupported error", async () => {
432
689
  mockListGroups.mockResolvedValue([
433
690
  { groupId: "group1", pinnedProvider: "prompt_api" },
434
691
  ]);
@@ -436,6 +693,7 @@ describe("invokeAgent", () => {
436
693
 
437
694
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
438
695
 
696
+ expect(mockEnsureBuiltinAiPolyfills).toHaveBeenCalled();
439
697
  expect(mockDeliverResponse).toHaveBeenCalledWith(
440
698
  mockOrchestrator,
441
699
  mockDb,
@@ -444,16 +702,66 @@ describe("invokeAgent", () => {
444
702
  );
445
703
  });
446
704
 
447
- it("should handle litert_lm_browser", async () => {
705
+ it("should handle prompt_api abort and errors", async () => {
706
+ mockListGroups.mockResolvedValue([
707
+ { groupId: "group1", pinnedProvider: "prompt_api" },
708
+ ]);
709
+ mockIsPromptApiSupported.mockReturnValue(true);
710
+
711
+ const abortErr = new Error("Abort");
712
+ abortErr.name = "AbortError";
713
+ mockInvokeWithPromptApi.mockRejectedValueOnce(abortErr);
714
+
715
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
716
+ expect(mockDeliverResponse).not.toHaveBeenCalled();
717
+
718
+ mockInvokeWithPromptApi.mockRejectedValueOnce(new Error("Prompt crash"));
719
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
720
+ expect(mockDeliverResponse).toHaveBeenCalledWith(
721
+ mockOrchestrator,
722
+ mockDb,
723
+ "group1",
724
+ expect.stringContaining("Prompt crash"),
725
+ );
726
+
727
+ mockInvokeWithPromptApi.mockRejectedValueOnce("prompt string error");
728
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
729
+ expect(mockDeliverResponse).toHaveBeenCalledWith(
730
+ mockOrchestrator,
731
+ mockDb,
732
+ "group1",
733
+ expect.stringContaining("prompt string error"),
734
+ );
735
+ });
736
+
737
+ it("should handle litert_lm_browser lifecycle, subagent invocation, and worker message", async () => {
448
738
  mockListGroups.mockResolvedValue([
449
739
  { groupId: "group1", pinnedProvider: "litert_lm_browser" },
450
740
  ]);
451
741
  mockIsLiteRtLmSupported.mockReturnValue(true);
452
- mockInvokeWithLiteRtLm.mockResolvedValue(undefined);
742
+ mockInvokeWithLiteRtLm.mockImplementation(async (...args: unknown[]) => {
743
+ const onMessage = args[5] as (msg: unknown) => Promise<void>;
744
+ const ctx = args[9] as {
745
+ invokeSubagent: (payload: unknown) => Promise<void>;
746
+ };
747
+ await onMessage({ type: "litert-chunk" });
748
+ await ctx.invokeSubagent({ prompt: "litert-sub" });
749
+ });
453
750
 
454
751
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
455
752
 
456
753
  expect(mockInvokeWithLiteRtLm).toHaveBeenCalled();
754
+ expect(mockHandleWorkerMessage).toHaveBeenCalledWith(
755
+ mockOrchestrator,
756
+ mockDb,
757
+ { type: "litert-chunk" },
758
+ );
759
+ expect(mockDispatchSubagentInvoke).toHaveBeenCalledWith(
760
+ mockDb,
761
+ { prompt: "litert-sub" },
762
+ expect.any(AbortSignal),
763
+ );
764
+ expect(mockOrchestrator.promptControllers.has("group1")).toBe(false);
457
765
  });
458
766
 
459
767
  it("should handle litert_lm_browser not supported", async () => {
@@ -472,10 +780,90 @@ describe("invokeAgent", () => {
472
780
  );
473
781
  });
474
782
 
783
+ it("should handle litert_lm_browser abort and errors", async () => {
784
+ mockListGroups.mockResolvedValue([
785
+ { groupId: "group1", pinnedProvider: "litert_lm_browser" },
786
+ ]);
787
+ mockIsLiteRtLmSupported.mockReturnValue(true);
788
+
789
+ const abortErr = new Error("Abort");
790
+ abortErr.name = "AbortError";
791
+ mockInvokeWithLiteRtLm.mockRejectedValueOnce(abortErr);
792
+
793
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
794
+ expect(mockDeliverResponse).not.toHaveBeenCalled();
795
+
796
+ mockInvokeWithLiteRtLm.mockRejectedValueOnce(new Error("LiteRT crash"));
797
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
798
+ expect(mockDeliverResponse).toHaveBeenCalledWith(
799
+ mockOrchestrator,
800
+ mockDb,
801
+ "group1",
802
+ expect.stringContaining("LiteRT crash"),
803
+ );
804
+
805
+ mockInvokeWithLiteRtLm.mockRejectedValueOnce("litert string err");
806
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
807
+ expect(mockDeliverResponse).toHaveBeenCalledWith(
808
+ mockOrchestrator,
809
+ mockDb,
810
+ "group1",
811
+ expect.stringContaining("litert string err"),
812
+ );
813
+ });
814
+
815
+ it("should stream when provider format is anthropic and streaming is enabled", async () => {
816
+ mockListGroups.mockResolvedValue([
817
+ { groupId: "group1", pinnedProvider: "anthropic-provider" },
818
+ ]);
819
+ mockGetProvider.mockImplementation(() => ({
820
+ defaultModel: "claude-3-haiku",
821
+ supportsStreaming: true,
822
+ format: "anthropic",
823
+ }));
824
+
825
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
826
+
827
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
828
+ expect.objectContaining({
829
+ payload: expect.objectContaining({
830
+ streaming: true,
831
+ }),
832
+ }),
833
+ );
834
+ });
835
+
836
+ it("should not stream when streaming is disabled or provider does not support it", async () => {
837
+ mockOrchestrator.streamingEnabled = false;
838
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
839
+
840
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
841
+ expect.objectContaining({
842
+ payload: expect.objectContaining({
843
+ streaming: false,
844
+ }),
845
+ }),
846
+ );
847
+ });
848
+
849
+ it("should mark isScheduledTask when group is in schedulerTriggeredGroups", async () => {
850
+ mockOrchestrator.schedulerTriggeredGroups.add("group1");
851
+
852
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
853
+
854
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
855
+ expect.objectContaining({
856
+ payload: expect.objectContaining({
857
+ isScheduledTask: true,
858
+ }),
859
+ }),
860
+ );
861
+ });
862
+
475
863
  it("should post message to worker for other providers", async () => {
476
864
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
477
865
 
478
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith({
866
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
479
867
  type: "invoke",
480
868
  payload: expect.objectContaining({
481
869
  apiKey: "key",
@@ -508,22 +896,46 @@ describe("invokeAgent", () => {
508
896
  );
509
897
  });
510
898
 
511
- it("should use pinned provider and model from group", async () => {
899
+ it("should use pinned provider and defaultModel when pinnedModel is not specified", async () => {
512
900
  mockListGroups.mockResolvedValue([
513
901
  {
514
902
  groupId: "group1",
515
903
  pinnedProvider: "pinned-provider",
516
- pinnedModel: "pinned-model",
517
904
  },
518
905
  ]);
906
+ mockGetProvider.mockReturnValue({
907
+ defaultModel: "provider-default-model",
908
+ supportsStreaming: true,
909
+ format: "openai",
910
+ });
519
911
 
520
912
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
521
913
 
522
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith({
914
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
523
915
  type: "invoke",
524
916
  payload: expect.objectContaining({
525
917
  provider: "pinned-provider",
526
- model: "pinned-model",
918
+ model: "provider-default-model",
919
+ }),
920
+ });
921
+ });
922
+
923
+ it("should fall back to orchestrator model when pinnedProvider lookup returns undefined", async () => {
924
+ mockListGroups.mockResolvedValue([
925
+ {
926
+ groupId: "group1",
927
+ pinnedProvider: "unknown-provider",
928
+ },
929
+ ]);
930
+ mockGetProvider.mockReturnValue(undefined);
931
+
932
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
933
+
934
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
935
+ type: "invoke",
936
+ payload: expect.objectContaining({
937
+ provider: "unknown-provider",
938
+ model: "test-model",
527
939
  }),
528
940
  });
529
941
  });
@@ -541,7 +953,7 @@ describe("invokeAgent", () => {
541
953
 
542
954
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
543
955
 
544
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith(
956
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
545
957
  expect.objectContaining({
546
958
  type: "invoke",
547
959
  payload: expect.objectContaining({
@@ -566,7 +978,7 @@ describe("invokeAgent", () => {
566
978
 
567
979
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
568
980
 
569
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith(
981
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
570
982
  expect.objectContaining({
571
983
  type: "invoke",
572
984
  payload: expect.objectContaining({
@@ -577,6 +989,29 @@ describe("invokeAgent", () => {
577
989
  );
578
990
  });
579
991
 
992
+ it("should fall back to orchestrator maxTokens when pinnedMaxTokens is invalid", async () => {
993
+ mockOrchestrator.maxTokens = 5000;
994
+ mockListGroups.mockResolvedValue([
995
+ {
996
+ groupId: "group1",
997
+ pinnedProvider: "test-provider",
998
+ pinnedModel: "test-model",
999
+ pinnedMaxTokens: -10,
1000
+ },
1001
+ ]);
1002
+
1003
+ await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
1004
+
1005
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
1006
+ expect.objectContaining({
1007
+ type: "invoke",
1008
+ payload: expect.objectContaining({
1009
+ maxTokens: 5000,
1010
+ }),
1011
+ }),
1012
+ );
1013
+ });
1014
+
580
1015
  it("should start transformers local polling", async () => {
581
1016
  mockListGroups.mockResolvedValue([
582
1017
  { groupId: "group1", pinnedProvider: "transformers_js_local" },
@@ -589,10 +1024,10 @@ describe("invokeAgent", () => {
589
1024
  mockOrchestrator.events,
590
1025
  "group1",
591
1026
  );
592
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalled();
1027
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalled();
593
1028
  });
594
1029
 
595
- it("should skip history when freshContext is true", async () => {
1030
+ it("should skip history when freshContext is true and messages exist", async () => {
596
1031
  mockBuildConversationMessages.mockResolvedValue([
597
1032
  { role: "user", content: "old message 1" },
598
1033
  { role: "assistant", content: "old message 2" },
@@ -613,10 +1048,27 @@ describe("invokeAgent", () => {
613
1048
  );
614
1049
  });
615
1050
 
1051
+ it("should seed freshContext with trigger message when conversation history is empty", async () => {
1052
+ mockBuildConversationMessages.mockResolvedValue([]);
1053
+
1054
+ await invokeAgent(
1055
+ mockOrchestrator,
1056
+ mockDb,
1057
+ "group1",
1058
+ "first trigger",
1059
+ true,
1060
+ );
1061
+
1062
+ expect(mockBuildDynamicContext).toHaveBeenCalledWith(
1063
+ [{ role: "user", content: "first trigger" }],
1064
+ expect.any(Object),
1065
+ );
1066
+ });
1067
+
616
1068
  it("should pass subagentTask true when subagent is true", async () => {
617
1069
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello", false, true);
618
1070
 
619
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith({
1071
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
620
1072
  type: "invoke",
621
1073
  payload: expect.objectContaining({
622
1074
  subagentTask: true,
@@ -628,28 +1080,25 @@ describe("invokeAgent", () => {
628
1080
  mockUlid.mockReturnValue("subagent-ulid");
629
1081
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello", false, true);
630
1082
 
631
- // Assert that thinking state and typing indicators are set on the subagent group ID
632
1083
  expect(mockOrchestrator.setState).toHaveBeenCalledWith(
633
1084
  "thinking",
634
1085
  "subagent:subagent-ulid",
635
1086
  );
636
- expect(mockOrchestrator.router.setTyping).toHaveBeenCalledWith(
1087
+ expect(mockOrchestrator.router?.setTyping).toHaveBeenCalledWith(
637
1088
  "subagent:subagent-ulid",
638
1089
  true,
639
1090
  );
640
1091
 
641
- // Assert that the parent group ID is NOT set to thinking or typing
642
1092
  expect(mockOrchestrator.setState).not.toHaveBeenCalledWith(
643
1093
  "thinking",
644
1094
  "group1",
645
1095
  );
646
- expect(mockOrchestrator.router.setTyping).not.toHaveBeenCalledWith(
1096
+ expect(mockOrchestrator.router?.setTyping).not.toHaveBeenCalledWith(
647
1097
  "group1",
648
1098
  true,
649
1099
  );
650
1100
 
651
- // Assert that the agent worker is invoked with the subagent group ID
652
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith({
1101
+ expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
653
1102
  type: "invoke",
654
1103
  payload: expect.objectContaining({
655
1104
  groupId: "subagent:subagent-ulid",
@@ -675,10 +1124,12 @@ describe("invokeAgent", () => {
675
1124
 
676
1125
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
677
1126
 
678
- const postCall = mockOrchestrator.agentWorker.postMessage.mock.calls[0][0];
679
- const enabledToolNames = postCall.payload.enabledTools.map(
680
- (t: any) => t.name,
681
- );
1127
+ const postCall = (
1128
+ mockOrchestrator.agentWorker?.postMessage as unknown as jest.Mock
1129
+ ).mock.calls[0][0] as {
1130
+ payload: { enabledTools: Array<{ name: string }> };
1131
+ };
1132
+ const enabledToolNames = postCall.payload.enabledTools.map((t) => t.name);
682
1133
  expect(enabledToolNames).toContain("generate_random_number");
683
1134
  expect(enabledToolNames).toContain("tool1");
684
1135
  expect(enabledToolNames).not.toContain("unpinned_declarative_tool");
@@ -700,10 +1151,12 @@ describe("invokeAgent", () => {
700
1151
 
701
1152
  await invokeAgent(mockOrchestrator, mockDb, "group1", "hello");
702
1153
 
703
- const postCall = mockOrchestrator.agentWorker.postMessage.mock.calls[0][0];
704
- const enabledToolNames = postCall.payload.enabledTools.map(
705
- (t: any) => t.name,
706
- );
1154
+ const postCall = (
1155
+ mockOrchestrator.agentWorker?.postMessage as unknown as jest.Mock
1156
+ ).mock.calls[0][0] as {
1157
+ payload: { enabledTools: Array<{ name: string }> };
1158
+ };
1159
+ const enabledToolNames = postCall.payload.enabledTools.map((t) => t.name);
707
1160
  expect(enabledToolNames).toContain("generate_random_number");
708
1161
  expect(enabledToolNames).not.toContain("disabled_decl_tool");
709
1162
  });