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
@@ -8,15 +8,44 @@ import {
8
8
  jest,
9
9
  } from "@jest/globals";
10
10
 
11
- import { CONFIG_KEYS } from "../../../config/config.js";
11
+ import {
12
+ ASSISTANT_NAME,
13
+ CONFIG_KEYS,
14
+ DEFAULT_MAX_ITERATIONS,
15
+ DEFAULT_PROMPT_API_FALLBACK_MODEL,
16
+ DEFAULT_TASK_SERVER_URL,
17
+ getDefaultProvider,
18
+ getModelMaxTokens,
19
+ } from "../../../config/config.js";
20
+ import type { ProviderConfig } from "../../../config/config.js";
21
+ import type { ShadowClawDatabase } from "../../../db/db.js";
22
+ import type { Task } from "../../../db/types.js";
23
+ import type { RoomInvitePayload } from "../../../subsystems/channels/peer-protocol.js";
24
+ import type {
25
+ InboundMessage,
26
+ RoomMember,
27
+ RoomMeta,
28
+ } from "../../../subsystems/channels/types.js";
29
+ import type { Orchestrator } from "../orchestrator.js";
30
+
31
+ const mockEnqueue = jest
32
+ .fn<
33
+ (
34
+ o: Orchestrator,
35
+ db: ShadowClawDatabase,
36
+ msg: InboundMessage,
37
+ ) => Promise<void>
38
+ >()
39
+ .mockResolvedValue(undefined);
12
40
 
13
- const mockEnqueue = (jest.fn() as any).mockResolvedValue(undefined);
14
41
  jest.unstable_mockModule("./enqueue.js", () => ({
15
42
  enqueue: mockEnqueue,
16
43
  }));
17
44
 
45
+ const mockApplyAllChannelRunningStates =
46
+ jest.fn<(state: Orchestrator) => void>();
18
47
  jest.unstable_mockModule("./operations/channel.js", () => ({
19
- applyAllChannelRunningStates: jest.fn(),
48
+ applyAllChannelRunningStates: mockApplyAllChannelRunningStates,
20
49
  applyChannelRunningState: jest.fn(),
21
50
  clearPeerJsTypingState: jest.fn(),
22
51
  getChannelByType: jest.fn(),
@@ -28,33 +57,140 @@ jest.unstable_mockModule("./operations/channel.js", () => ({
28
57
  shouldRunChannel: jest.fn(),
29
58
  submitMessage: jest.fn(),
30
59
  }));
60
+
61
+ const mockRunTaskAsScheduled = jest
62
+ .fn<(state: Orchestrator, task: Task) => Promise<void>>()
63
+ .mockResolvedValue(undefined);
64
+ const mockShouldStartLocalScheduler = jest
65
+ .fn<() => Promise<boolean>>()
66
+ .mockResolvedValue(true);
31
67
  jest.unstable_mockModule("./operations/task.js", () => ({
32
68
  deleteTaskFromServer: jest.fn(),
33
- runTaskAsScheduled: jest.fn(),
34
- shouldStartLocalScheduler: (jest.fn() as any).mockResolvedValue(true),
69
+ runTaskAsScheduled: mockRunTaskAsScheduled,
70
+ shouldStartLocalScheduler: mockShouldStartLocalScheduler,
35
71
  syncTaskToServer: jest.fn(),
36
72
  warnIfNoPushSubscription: jest.fn(),
37
73
  }));
38
74
 
39
- const mockDeleteRoom = (jest.fn() as any).mockResolvedValue(undefined);
40
- const mockFetchModelInfo = (jest.fn() as any).mockResolvedValue(undefined);
41
- const mockGetConfig = jest.fn() as any;
42
- const mockGetRoomMetadata = jest.fn() as any;
43
- const mockRoomManager = (jest.fn() as any).mockImplementation(
44
- (config: any) => ({ loadRooms: mockRoomManagerLoadRooms, config }),
45
- );
46
- const mockRoomManagerLoadRooms = jest.fn() as any;
47
- const mockSetConfig = jest.fn() as any;
48
- const mockSetRemoteAgentTyping = jest.fn() as any;
49
- const mockSetWebMcpMode = jest.fn() as any;
50
- const mockTaskScheduler = (jest.fn() as any).mockImplementation(() => ({
51
- start: mockTaskSchedulerStart,
75
+ const mockDeleteRoom = jest
76
+ .fn<(db: ShadowClawDatabase, id: string) => Promise<void>>()
77
+ .mockResolvedValue(undefined);
78
+ const mockFetchModelInfo = jest
79
+ .fn<() => Promise<void>>()
80
+ .mockResolvedValue(undefined);
81
+ const mockGetConfig =
82
+ jest.fn<
83
+ (db: ShadowClawDatabase, key: string) => Promise<string | undefined>
84
+ >();
85
+ const mockGetRoomMetadata = jest
86
+ .fn<(db: ShadowClawDatabase) => Promise<RoomMeta[]>>()
87
+ .mockResolvedValue([]);
88
+ const mockRoomManagerLoadRooms = jest.fn<(rooms: RoomMeta[]) => void>();
89
+ const mockRoomManagerInstance = {
90
+ loadRooms: mockRoomManagerLoadRooms,
91
+ createRoom: jest.fn(),
92
+ invite: jest.fn(),
93
+ joinRoom: jest.fn(),
94
+ leaveRoom: jest.fn(),
95
+ list: jest.fn(),
96
+ };
97
+ let capturedRoomManagerConfig: {
98
+ transport: {
99
+ myPeerId: string;
100
+ sendToPeer: (peerId: string, note: unknown) => void;
101
+ isConnected: (peerId: string) => boolean;
102
+ connectToPeer: (peerId: string) => void;
103
+ };
104
+ getLocalMember: () => RoomMember;
105
+ onMessage: (msg: InboundMessage) => void;
106
+ onInvite: (invite: RoomInvitePayload) => void;
107
+ persistRoom: (room: RoomMeta) => void;
108
+ removeRoom: (roomId: string) => void;
109
+ } | null = null;
110
+
111
+ const mockRoomManager = jest
112
+ .fn<
113
+ (config: typeof capturedRoomManagerConfig) => typeof mockRoomManagerInstance
114
+ >()
115
+ .mockImplementation((config) => {
116
+ capturedRoomManagerConfig = config;
117
+ return mockRoomManagerInstance;
118
+ });
119
+
120
+ const mockSetConfig = jest
121
+ .fn<(db: ShadowClawDatabase, key: string, value: string) => Promise<void>>()
122
+ .mockResolvedValue(undefined);
123
+ const mockSetRemoteAgentTyping =
124
+ jest.fn<(groupId: string, typing: boolean) => void>();
125
+ const mockSetWebMcpMode = jest.fn<(mode: string) => void>();
126
+ const mockTaskSchedulerStart = jest.fn<() => void>();
127
+ let capturedTaskExecutor: ((task: Task) => Promise<void>) | null = null;
128
+ let capturedTaskOnExecuted: (() => void) | null = null;
129
+
130
+ const mockTaskScheduler = jest
131
+ .fn<
132
+ (
133
+ executor: (task: Task) => Promise<void>,
134
+ onExecuted: () => void,
135
+ ) => { start: () => void; stop: () => void }
136
+ >()
137
+ .mockImplementation((executor, onExecuted) => {
138
+ capturedTaskExecutor = executor;
139
+ capturedTaskOnExecuted = onExecuted;
140
+ return {
141
+ start: mockTaskSchedulerStart,
142
+ stop: jest.fn(),
143
+ };
144
+ });
145
+
146
+ const mockToTrustedScriptUrl = jest
147
+ .fn<(url: string) => string>()
148
+ .mockImplementation((url: string) => url);
149
+ const mockUpsertRoom = jest
150
+ .fn<(db: ShadowClawDatabase, room: RoomMeta) => Promise<void>>()
151
+ .mockResolvedValue(undefined);
152
+ const mockHandleRoomInvite =
153
+ jest.fn<(state: unknown, invite: RoomInvitePayload) => void>();
154
+
155
+ const mockApplyLlamafileHeaders = jest.fn<(o: Orchestrator) => void>();
156
+ const mockApplyMeshLlmHeaders = jest.fn<(o: Orchestrator) => void>();
157
+ const mockGetApiKeyForHeaders = jest
158
+ .fn<(o: Orchestrator) => Promise<string | null>>()
159
+ .mockResolvedValue("key");
160
+ const mockGetProviderRuntimeHeaders = jest
161
+ .fn<(o: Orchestrator, prov: string) => Record<string, string>>()
162
+ .mockReturnValue({});
163
+
164
+ jest.unstable_mockModule("./operations/provider.js", () => ({
165
+ applyLlamafileHeaders: mockApplyLlamafileHeaders,
166
+ applyMeshLlmHeaders: mockApplyMeshLlmHeaders,
167
+ getApiKeyForHeaders: mockGetApiKeyForHeaders,
168
+ getProviderRuntimeHeaders: mockGetProviderRuntimeHeaders,
52
169
  }));
53
- const mockTaskSchedulerStart = jest.fn() as any;
54
- const mockToTrustedScriptUrl = (jest.fn() as any).mockImplementation(
55
- (url: string) => url,
170
+
171
+ jest.unstable_mockModule("./operations/room.js", () => ({
172
+ handleRoomInvite: mockHandleRoomInvite,
173
+ }));
174
+
175
+ const mockIsPromptApiSupported = jest
176
+ .fn<() => boolean>()
177
+ .mockReturnValue(false);
178
+ jest.unstable_mockModule(
179
+ "../../../subsystems/providers/prompt-api-provider.js",
180
+ () => ({
181
+ isPromptApiSupported: mockIsPromptApiSupported,
182
+ }),
183
+ );
184
+
185
+ const mockFetchTokenizerConfig = jest
186
+ .fn<(model: string) => Promise<unknown>>()
187
+ .mockResolvedValue({});
188
+ jest.unstable_mockModule(
189
+ "../../../subsystems/providers/utils/chatTemplate.js",
190
+ () => ({
191
+ fetchTokenizerConfig: mockFetchTokenizerConfig,
192
+ }),
56
193
  );
57
- const mockUpsertRoom = (jest.fn() as any).mockResolvedValue(undefined);
58
194
 
59
195
  jest.unstable_mockModule("../../../db/getConfig.js", () => ({
60
196
  getConfig: mockGetConfig,
@@ -107,33 +243,33 @@ jest.unstable_mockModule("../../../security/trusted-types.js", () => ({
107
243
  toTrustedScriptUrl: mockToTrustedScriptUrl,
108
244
  }));
109
245
 
246
+ const mockSyncProxyConfigToServiceWorker =
247
+ jest.fn<(state: Orchestrator) => void>();
110
248
  jest.unstable_mockModule("./syncProxyConfigToServiceWorker.js", () => ({
111
- syncProxyConfigToServiceWorker: jest.fn(),
249
+ syncProxyConfigToServiceWorker: mockSyncProxyConfigToServiceWorker,
112
250
  }));
113
251
 
252
+ const mockLoadChannelConfigurations = jest
253
+ .fn<(state: Orchestrator, db: ShadowClawDatabase) => Promise<void>>()
254
+ .mockResolvedValue(undefined);
114
255
  jest.unstable_mockModule("./loadChannelConfigurations.js", () => ({
115
- loadChannelConfigurations: jest.fn<any>().mockResolvedValue(undefined),
256
+ loadChannelConfigurations: mockLoadChannelConfigurations,
116
257
  }));
117
258
 
118
- const mockSetupPushTaskListener = jest.fn();
259
+ const mockSetupPushTaskListener =
260
+ jest.fn<(orchestrator: Orchestrator, db: ShadowClawDatabase) => void>();
119
261
  jest.unstable_mockModule("./setupPushTaskListener.js", () => ({
120
262
  setupPushTaskListener: mockSetupPushTaskListener,
121
263
  }));
122
264
 
123
- const mockHandleWorkerMessage = jest.fn();
265
+ const mockHandleWorkerMessage =
266
+ jest.fn<
267
+ (orchestrator: Orchestrator, db: ShadowClawDatabase, msg: unknown) => void
268
+ >();
124
269
  jest.unstable_mockModule("./handleWorkerMessage.js", () => ({
125
270
  handleWorkerMessage: mockHandleWorkerMessage,
126
271
  }));
127
272
 
128
- const { syncProxyConfigToServiceWorker } =
129
- await import("./syncProxyConfigToServiceWorker.js");
130
- const { loadChannelConfigurations } =
131
- await import("./loadChannelConfigurations.js");
132
-
133
- const { applyAllChannelRunningStates } =
134
- await import("./operations/channel.js");
135
- const { runTaskAsScheduled } = await import("./operations/task.js");
136
-
137
273
  const {
138
274
  createRoomManager,
139
275
  initChannelsAndRooms,
@@ -144,53 +280,127 @@ const {
144
280
  initWorkerAndScheduler,
145
281
  } = await import("./initTasks.js");
146
282
 
147
- const { TaskScheduler } =
148
- await import("../../../subsystems/tools/task-scheduler.js");
149
-
150
- const { RoomManager } =
151
- await import("../../../subsystems/channels/room-manager.js");
152
-
153
283
  class MockWorker {
154
- onmessage: any = null;
155
- onerror: any = null;
156
- postMessage = jest.fn();
284
+ onmessage: ((event: { data: unknown }) => void) | null = null;
285
+ onerror: ((err: unknown) => void) | null = null;
286
+ postMessage = jest.fn<(_data: unknown) => void>();
157
287
  constructor(
158
288
  public scriptURL: string | URL,
159
289
  public options?: WorkerOptions,
160
290
  ) {}
161
291
  }
162
292
 
293
+ interface MockPeerJs {
294
+ myPeerId: string;
295
+ sendRoomNotification: jest.Mock<(_peerId: string, _note: unknown) => void>;
296
+ isPeerConnected: jest.Mock<(_peerId: string) => boolean>;
297
+ connectPeer: jest.Mock<(_peerId: string) => void>;
298
+ onTaskComplete: jest.Mock<(_cb: (_groupId: string) => void) => void>;
299
+ }
300
+
301
+ interface MockChannelRegistry {
302
+ onMessage: jest.Mock<(_cb: (_msg: InboundMessage) => void) => void>;
303
+ onTyping: jest.Mock<
304
+ (_cb: (_groupId: string, _typing: boolean) => void) => void
305
+ >;
306
+ }
307
+
308
+ interface MockInitOrchestrator {
309
+ peerjs: MockPeerJs;
310
+ peerjsMyPeerId: string;
311
+ peerjsMyAlias: string;
312
+ assistantName: string;
313
+ roomChannel: { deliverInbound: jest.Mock<(_msg: InboundMessage) => void> };
314
+ handleRoomInvite: jest.Mock;
315
+ db: ShadowClawDatabase | null;
316
+ channelRegistry: MockChannelRegistry;
317
+ events: { emit: jest.Mock<(_event: string, _payload: unknown) => void> };
318
+ peerCompletedContexts: Set<string>;
319
+ roomManager: { loadRooms: jest.Mock<(_rooms: RoomMeta[]) => void> };
320
+ initializeChannelRegistry: jest.Mock<() => void>;
321
+ applyLlamafileHeaders: jest.Mock<() => void>;
322
+ applyMeshLlmHeaders: jest.Mock<() => void>;
323
+ getApiKeyForHeaders: jest.Mock<() => Promise<string | null>>;
324
+ getApiKey: jest.Mock<() => Promise<string | null>>;
325
+ getProviderRuntimeHeaders: jest.Mock<() => Record<string, string>>;
326
+ loadApiKeyForProvider: jest.Mock<
327
+ (_db: ShadowClawDatabase, _provider: string) => Promise<void>
328
+ >;
329
+ runTaskAsScheduled: jest.Mock;
330
+ shouldStartLocalScheduler: jest.Mock;
331
+ setupPushTaskListener: jest.Mock;
332
+ handleWorkerMessage: jest.Mock;
333
+ provider: string;
334
+ providerConfig: ProviderConfig;
335
+ model: string;
336
+ maxTokens: number;
337
+ maxIterations: number;
338
+ rateLimitCallsPerMinute: number;
339
+ rateLimitAutoAdapt: boolean;
340
+ bedrockRegionFallback: string;
341
+ bedrockProfileFallback: string;
342
+ bedrockAuthMode: string;
343
+ agentWorker?: MockWorker;
344
+ scheduler?: { start: jest.Mock<() => void>; stop: jest.Mock<() => void> };
345
+ vmBootMode?: string;
346
+ streamingEnabled?: boolean;
347
+ webMcpToolsEnabled?: boolean;
348
+ vmBashFullInternetAccess?: boolean;
349
+ contextCompressionEnabled?: boolean;
350
+ reasoningEffort?: string;
351
+ directToolCommandPolicy?: unknown;
352
+ useProxy?: boolean;
353
+ proxyUrl?: string;
354
+ gitProxyUrl?: string;
355
+ taskServerEnabled?: boolean;
356
+ taskServerUrl?: string;
357
+ llamafileMode?: string;
358
+ llamafileHost?: string;
359
+ llamafilePort?: number;
360
+ llamafileOffline?: boolean;
361
+ meshLlmHost?: string;
362
+ triggerPattern?: RegExp;
363
+ }
364
+
163
365
  describe("initTasks", () => {
164
- let mockOrchestrator: any;
165
- let mockDb: any;
166
- let originalWorker: any;
167
- let originalRequestIdleCallback: any;
366
+ let mockOrchestrator: MockInitOrchestrator;
367
+ const mockDb = {} as ShadowClawDatabase;
368
+ const originalWorker = global.Worker;
369
+ const originalRequestIdleCallback = (
370
+ globalThis as unknown as { requestIdleCallback?: unknown }
371
+ ).requestIdleCallback;
168
372
 
169
373
  beforeAll(() => {
170
- originalWorker = global.Worker;
171
- (global as any).Worker = MockWorker;
172
-
173
- originalRequestIdleCallback = (global as any).requestIdleCallback;
174
- (global as any).requestIdleCallback = (cb: any) => cb();
374
+ (globalThis as unknown as { Worker: unknown }).Worker = MockWorker;
375
+ (
376
+ globalThis as unknown as { requestIdleCallback: unknown }
377
+ ).requestIdleCallback = (cb: () => void) => cb();
175
378
  });
176
379
 
177
380
  afterAll(() => {
178
- (global as any).Worker = originalWorker;
179
- (global as any).requestIdleCallback = originalRequestIdleCallback;
381
+ (globalThis as unknown as { Worker: unknown }).Worker = originalWorker;
382
+ (
383
+ globalThis as unknown as { requestIdleCallback: unknown }
384
+ ).requestIdleCallback = originalRequestIdleCallback;
180
385
  });
181
386
 
182
387
  beforeEach(() => {
183
388
  jest.clearAllMocks();
389
+ capturedRoomManagerConfig = null;
390
+ capturedTaskExecutor = null;
391
+ capturedTaskOnExecuted = null;
184
392
 
185
- mockDb = {};
186
393
  mockOrchestrator = {
187
394
  peerjs: {
188
395
  myPeerId: "peer1",
189
396
  sendRoomNotification: jest.fn(),
190
- isPeerConnected: jest.fn(),
397
+ isPeerConnected: jest
398
+ .fn<(_peerId: string) => boolean>()
399
+ .mockReturnValue(true),
191
400
  connectPeer: jest.fn(),
192
401
  onTaskComplete: jest.fn(),
193
402
  },
403
+ peerjsMyPeerId: "peer1",
194
404
  peerjsMyAlias: "my-alias",
195
405
  assistantName: "Assistant",
196
406
  roomChannel: { deliverInbound: jest.fn() },
@@ -200,42 +410,66 @@ describe("initTasks", () => {
200
410
  onMessage: jest.fn(),
201
411
  onTyping: jest.fn(),
202
412
  },
203
- enqueue: (jest.fn() as any).mockResolvedValue(undefined),
204
413
  events: { emit: jest.fn() },
205
- peerCompletedContexts: new Set(),
206
- applyAllChannelRunningStates: jest.fn(),
207
- roomManager: { loadRooms: jest.fn() },
414
+ peerCompletedContexts: new Set<string>(),
415
+ roomManager: { loadRooms: mockRoomManagerLoadRooms },
208
416
  initializeChannelRegistry: jest.fn(),
209
417
  applyLlamafileHeaders: jest.fn(),
210
418
  applyMeshLlmHeaders: jest.fn(),
211
- getApiKeyForHeaders: (jest.fn() as any).mockResolvedValue("key"),
212
- getApiKey: (jest.fn() as any).mockResolvedValue("key"),
213
- getProviderRuntimeHeaders: jest.fn().mockReturnValue({}),
214
- loadApiKeyForProvider: (jest.fn() as any).mockResolvedValue(undefined),
215
- runTaskAsScheduled: (jest.fn() as any).mockResolvedValue(undefined),
216
- shouldStartLocalScheduler: (jest.fn() as any).mockResolvedValue(true),
419
+ getApiKeyForHeaders: jest
420
+ .fn<() => Promise<string | null>>()
421
+ .mockResolvedValue("key"),
422
+ getApiKey: jest
423
+ .fn<() => Promise<string | null>>()
424
+ .mockResolvedValue("key"),
425
+ getProviderRuntimeHeaders: jest
426
+ .fn<() => Record<string, string>>()
427
+ .mockReturnValue({}),
428
+ loadApiKeyForProvider: jest
429
+ .fn<(_db: ShadowClawDatabase, _provider: string) => Promise<void>>()
430
+ .mockResolvedValue(undefined),
431
+ runTaskAsScheduled: jest.fn(),
432
+ shouldStartLocalScheduler: jest.fn(),
217
433
  setupPushTaskListener: jest.fn(),
218
434
  handleWorkerMessage: jest.fn(),
219
- providerConfig: { defaultModel: "default-model" },
435
+ provider: "openrouter",
436
+ providerConfig: getDefaultProvider(),
437
+ model: "test-model",
438
+ maxTokens: 4096,
439
+ maxIterations: DEFAULT_MAX_ITERATIONS,
440
+ rateLimitCallsPerMinute: 0,
441
+ rateLimitAutoAdapt: true,
442
+ bedrockRegionFallback: "",
443
+ bedrockProfileFallback: "",
444
+ bedrockAuthMode: "provider_chain",
220
445
  };
221
446
 
222
- (mockGetConfig as any).mockResolvedValue(null);
447
+ mockGetConfig.mockResolvedValue(undefined);
223
448
  });
224
449
 
225
450
  describe("createRoomManager", () => {
226
- it("should create a room manager with correct transport and handlers", () => {
227
- const roomManager = createRoomManager(mockOrchestrator) as any;
228
- expect(RoomManager).toHaveBeenCalled();
451
+ it("should create room manager with transport methods and local member", () => {
452
+ createRoomManager(mockOrchestrator as unknown as Orchestrator);
453
+ expect(mockRoomManager).toHaveBeenCalled();
454
+ expect(capturedRoomManagerConfig).not.toBeNull();
229
455
 
230
- const config = roomManager.config;
456
+ const config = capturedRoomManagerConfig!;
231
457
  expect(config.transport.myPeerId).toBe("peer1");
232
458
 
233
- config.transport.sendToPeer("peer2", {} as any);
459
+ config.transport.sendToPeer("peer2", { type: "test" });
234
460
  expect(mockOrchestrator.peerjs.sendRoomNotification).toHaveBeenCalledWith(
235
461
  "peer2",
236
- {},
462
+ { type: "test" },
463
+ );
464
+
465
+ expect(config.transport.isConnected("peer2")).toBe(true);
466
+ expect(mockOrchestrator.peerjs.isPeerConnected).toHaveBeenCalledWith(
467
+ "peer2",
237
468
  );
238
469
 
470
+ config.transport.connectToPeer("peer2");
471
+ expect(mockOrchestrator.peerjs.connectPeer).toHaveBeenCalledWith("peer2");
472
+
239
473
  const member = config.getLocalMember();
240
474
  expect(member).toEqual({
241
475
  peerId: "peer1",
@@ -244,200 +478,701 @@ describe("initTasks", () => {
244
478
  agentName: "Assistant",
245
479
  });
246
480
 
247
- config.onMessage({ id: "msg1" } as any);
248
- expect(mockOrchestrator.roomChannel.deliverInbound).toHaveBeenCalledWith({
249
- id: "msg1",
250
- });
481
+ const inboundMsg: InboundMessage = {
482
+ id: "msg-1",
483
+ groupId: "room:r1",
484
+ sender: "peer2",
485
+ content: "hello",
486
+ timestamp: Date.now(),
487
+ channel: "room",
488
+ };
489
+ config.onMessage(inboundMsg);
490
+ expect(mockOrchestrator.roomChannel.deliverInbound).toHaveBeenCalledWith(
491
+ inboundMsg,
492
+ );
251
493
 
252
- config.persistRoom({ id: "room1" });
253
- expect(mockUpsertRoom).toHaveBeenCalledWith(mockDb, { id: "room1" });
494
+ const invitePayload: RoomInvitePayload = {
495
+ roomId: "r1",
496
+ roomName: "Dev Room",
497
+ hostPeerId: "peer1",
498
+ fromPeerId: "peer1",
499
+ };
500
+ config.onInvite(invitePayload);
501
+ expect(mockHandleRoomInvite).toHaveBeenCalledWith(
502
+ mockOrchestrator,
503
+ invitePayload,
504
+ );
505
+ });
254
506
 
255
- config.removeRoom("room1");
256
- expect(mockDeleteRoom).toHaveBeenCalledWith(mockDb, "room1");
507
+ it("should test getLocalMember fallback branches and transport fallback myPeerId", () => {
508
+ mockOrchestrator.peerjs.myPeerId = "";
509
+ mockOrchestrator.peerjsMyPeerId = "fallback-peer-id";
510
+ mockOrchestrator.peerjsMyAlias = "";
511
+
512
+ createRoomManager(mockOrchestrator as unknown as Orchestrator);
513
+ const config = capturedRoomManagerConfig!;
514
+
515
+ expect(config.transport.myPeerId).toBe("fallback-peer-id");
516
+
517
+ const memberWithPeerIdAlias = config.getLocalMember();
518
+ expect(memberWithPeerIdAlias.alias).toBe("fallback-peer-id");
519
+
520
+ mockOrchestrator.peerjsMyPeerId = "";
521
+ const memberWithAssistantNameAlias = config.getLocalMember();
522
+ expect(memberWithAssistantNameAlias.alias).toBe("Assistant");
523
+ });
524
+
525
+ it("should handle persistRoom and removeRoom with success, null db, and catch errors", async () => {
526
+ createRoomManager(mockOrchestrator as unknown as Orchestrator);
527
+ const config = capturedRoomManagerConfig!;
528
+ const room: RoomMeta = {
529
+ roomId: "r1",
530
+ name: "Test Room",
531
+ hostPeerId: "peer1",
532
+ createdAt: Date.now(),
533
+ members: [],
534
+ };
535
+
536
+ config.persistRoom(room);
537
+ expect(mockUpsertRoom).toHaveBeenCalledWith(mockDb, room);
538
+
539
+ config.removeRoom("r1");
540
+ expect(mockDeleteRoom).toHaveBeenCalledWith(mockDb, "r1");
541
+
542
+ const consoleErrorSpy = jest
543
+ .spyOn(console, "error")
544
+ .mockImplementation(() => {});
545
+
546
+ mockUpsertRoom.mockRejectedValueOnce(new Error("Persist fail"));
547
+ config.persistRoom(room);
548
+ await Promise.resolve();
549
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
550
+ "Failed to persist room:",
551
+ expect.any(Error),
552
+ );
553
+
554
+ mockDeleteRoom.mockRejectedValueOnce(new Error("Delete fail"));
555
+ config.removeRoom("r1");
556
+ await Promise.resolve();
557
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
558
+ "Failed to delete room:",
559
+ expect.any(Error),
560
+ );
561
+
562
+ mockOrchestrator.db = null;
563
+ mockUpsertRoom.mockClear();
564
+ mockDeleteRoom.mockClear();
565
+ config.persistRoom(room);
566
+ config.removeRoom("r1");
567
+ expect(mockUpsertRoom).not.toHaveBeenCalled();
568
+ expect(mockDeleteRoom).not.toHaveBeenCalled();
569
+
570
+ consoleErrorSpy.mockRestore();
257
571
  });
258
572
  });
259
573
 
260
574
  describe("initChannelsAndRooms", () => {
261
- it("should initialize channels, events, and rooms", async () => {
262
- (mockGetRoomMetadata as any).mockResolvedValue([{ id: "room1" }]);
263
-
264
- await initChannelsAndRooms(mockOrchestrator, mockDb);
575
+ it("should initialize channel registry, callbacks, configurations, and rooms", async () => {
576
+ const room1: RoomMeta = {
577
+ roomId: "room1",
578
+ name: "Room 1",
579
+ hostPeerId: "peer1",
580
+ createdAt: Date.now(),
581
+ members: [],
582
+ };
583
+ mockGetRoomMetadata.mockResolvedValueOnce([room1]);
265
584
 
266
- expect(loadChannelConfigurations).toHaveBeenCalledWith(
267
- mockOrchestrator,
585
+ await initChannelsAndRooms(
586
+ mockOrchestrator as unknown as Orchestrator,
268
587
  mockDb,
269
588
  );
589
+
270
590
  expect(mockOrchestrator.initializeChannelRegistry).toHaveBeenCalled();
271
- expect(mockOrchestrator.channelRegistry.onMessage).toHaveBeenCalled();
272
- expect(mockOrchestrator.channelRegistry.onTyping).toHaveBeenCalled();
273
- expect(mockOrchestrator.peerjs.onTaskComplete).toHaveBeenCalled();
274
- expect(applyAllChannelRunningStates).toHaveBeenCalled();
591
+ expect(mockLoadChannelConfigurations).toHaveBeenCalledWith(
592
+ mockOrchestrator as unknown as Orchestrator,
593
+ mockDb,
594
+ );
595
+ expect(mockApplyAllChannelRunningStates).toHaveBeenCalledWith(
596
+ mockOrchestrator as unknown as Orchestrator,
597
+ );
598
+ expect(mockRoomManagerLoadRooms).toHaveBeenCalledWith([room1]);
275
599
 
276
- // Simulate onMessage callback
600
+ // onMessage callback
277
601
  const onMessageCb =
278
602
  mockOrchestrator.channelRegistry.onMessage.mock.calls[0][0];
279
- onMessageCb({ id: "msg1" });
280
- expect(mockEnqueue).toHaveBeenCalledWith(mockOrchestrator, mockDb, {
603
+ const testMsg: InboundMessage = {
281
604
  id: "msg1",
282
- });
605
+ groupId: "g1",
606
+ sender: "user",
607
+ content: "hi",
608
+ timestamp: Date.now(),
609
+ channel: "browser",
610
+ };
611
+ onMessageCb(testMsg);
612
+ expect(mockEnqueue).toHaveBeenCalledWith(
613
+ mockOrchestrator as unknown as Orchestrator,
614
+ mockDb,
615
+ testMsg,
616
+ );
283
617
 
284
- // Simulate onTyping callback
618
+ // onTyping callback for peer and non-peer groups
285
619
  const onTypingCb =
286
620
  mockOrchestrator.channelRegistry.onTyping.mock.calls[0][0];
287
- onTypingCb("peer:123", true);
621
+ onTypingCb("peer:p1", true);
288
622
  expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("typing", {
289
- groupId: "peer:123",
623
+ groupId: "peer:p1",
290
624
  typing: true,
291
625
  });
292
- expect(mockSetRemoteAgentTyping).toHaveBeenCalledWith("peer:123", true);
626
+ expect(mockSetRemoteAgentTyping).toHaveBeenCalledWith("peer:p1", true);
293
627
 
294
- // Simulate onTaskComplete callback
628
+ mockSetRemoteAgentTyping.mockClear();
629
+ onTypingCb("browser-group", false);
630
+ expect(mockSetRemoteAgentTyping).not.toHaveBeenCalled();
631
+
632
+ // onTaskComplete callback
295
633
  const onTaskCompleteCb =
296
634
  mockOrchestrator.peerjs.onTaskComplete.mock.calls[0][0];
297
- onTaskCompleteCb("group1");
298
- expect(mockOrchestrator.peerCompletedContexts.has("group1")).toBe(true);
635
+ onTaskCompleteCb("peer:completed-group");
636
+ expect(
637
+ mockOrchestrator.peerCompletedContexts.has("peer:completed-group"),
638
+ ).toBe(true);
639
+ });
640
+
641
+ it("should handle error in onMessage enqueue and error in getRoomMetadata", async () => {
642
+ const consoleErrorSpy = jest
643
+ .spyOn(console, "error")
644
+ .mockImplementation(() => {});
299
645
 
300
- expect(mockGetRoomMetadata).toHaveBeenCalledWith(mockDb);
301
- expect(mockOrchestrator.roomManager.loadRooms).toHaveBeenCalledWith([
302
- { id: "room1" },
303
- ]);
646
+ mockGetRoomMetadata.mockRejectedValueOnce(new Error("DB read error"));
647
+ await initChannelsAndRooms(
648
+ mockOrchestrator as unknown as Orchestrator,
649
+ mockDb,
650
+ );
651
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
652
+ "Failed to load rooms:",
653
+ expect.any(Error),
654
+ );
655
+
656
+ mockEnqueue.mockRejectedValueOnce(new Error("Enqueue reject"));
657
+ const onMessageCb =
658
+ mockOrchestrator.channelRegistry.onMessage.mock.calls[0][0];
659
+ onMessageCb({ id: "m" } as unknown as InboundMessage);
660
+ await Promise.resolve();
661
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
662
+ "Failed to enqueue inbound message:",
663
+ expect.any(Error),
664
+ );
665
+
666
+ consoleErrorSpy.mockRestore();
304
667
  });
305
668
  });
306
669
 
307
670
  describe("initCoreConfig", () => {
308
- it("should load core config correctly", async () => {
309
- mockGetConfig.mockResolvedValueOnce("My Bot");
310
- await initCoreConfig(mockOrchestrator, mockDb);
311
- expect(mockOrchestrator.assistantName).toBe("My Bot");
671
+ it("should load assistant name from config or default", async () => {
672
+ mockGetConfig.mockResolvedValueOnce("CustomBot");
673
+ await initCoreConfig(mockOrchestrator as unknown as Orchestrator, mockDb);
674
+ expect(mockOrchestrator.assistantName).toBe("CustomBot");
312
675
  expect(mockOrchestrator.triggerPattern).toBeDefined();
676
+
677
+ mockGetConfig.mockResolvedValueOnce(undefined);
678
+ await initCoreConfig(mockOrchestrator as unknown as Orchestrator, mockDb);
679
+ expect(mockOrchestrator.assistantName).toBe(ASSISTANT_NAME);
313
680
  });
314
681
  });
315
682
 
316
683
  describe("initFeatureFlagsAndLimits", () => {
317
- it("should load feature flags correctly", async () => {
318
- mockGetConfig.mockImplementation(async (_db: any, key: any) => {
319
- if (key === CONFIG_KEYS.VM_BOOT_MODE) return "ext2";
320
- if (key === CONFIG_KEYS.STREAMING_ENABLED) return "true";
321
- if (key === CONFIG_KEYS.WEBMCP_TOOLS_ENABLED) return "false";
322
- if (key === CONFIG_KEYS.VM_BASH_FULL_INTERNET_ACCESS) return "true";
323
- if (key === CONFIG_KEYS.WEBMCP_MODE) return "native";
324
- if (key === CONFIG_KEYS.CONTEXT_COMPRESSION_ENABLED) return "true";
325
- if (key === CONFIG_KEYS.REASONING_EFFORT) return "High";
326
- if (key === CONFIG_KEYS.USE_PROXY) return "true";
327
- if (key === CONFIG_KEYS.PROXY_URL) return "http://proxy";
328
- return null;
684
+ it("should load all feature flags, modes, and limits", async () => {
685
+ mockGetConfig.mockImplementation(async (_db, key) => {
686
+ switch (key) {
687
+ case CONFIG_KEYS.VM_BOOT_MODE:
688
+ return "9p";
689
+ case CONFIG_KEYS.STREAMING_ENABLED:
690
+ return "false";
691
+ case CONFIG_KEYS.WEBMCP_TOOLS_ENABLED:
692
+ return "false";
693
+ case CONFIG_KEYS.VM_BASH_FULL_INTERNET_ACCESS:
694
+ return "true";
695
+ case CONFIG_KEYS.WEBMCP_MODE:
696
+ return "polyfill";
697
+ case CONFIG_KEYS.CONTEXT_COMPRESSION_ENABLED:
698
+ return "true";
699
+ case CONFIG_KEYS.REASONING_EFFORT:
700
+ return " Medium ";
701
+ case CONFIG_KEYS.USE_PROXY:
702
+ return "true";
703
+ case CONFIG_KEYS.PROXY_URL:
704
+ return "http://proxy.local";
705
+ case CONFIG_KEYS.GIT_PROXY_URL:
706
+ return "http://git-proxy.local";
707
+ case CONFIG_KEYS.TASK_SERVER_ENABLED:
708
+ return "true";
709
+ case CONFIG_KEYS.TASK_SERVER_URL:
710
+ return "http://tasks.local";
711
+ default:
712
+ return undefined;
713
+ }
329
714
  });
330
715
 
331
- await initFeatureFlagsAndLimits(mockOrchestrator, mockDb);
716
+ await initFeatureFlagsAndLimits(
717
+ mockOrchestrator as unknown as Orchestrator,
718
+ mockDb,
719
+ );
332
720
 
333
- expect(mockOrchestrator.vmBootMode).toBe("ext2");
334
- expect(mockOrchestrator.streamingEnabled).toBe(true);
721
+ expect(mockOrchestrator.vmBootMode).toBe("9p");
722
+ expect(mockOrchestrator.streamingEnabled).toBe(false);
335
723
  expect(mockOrchestrator.webMcpToolsEnabled).toBe(false);
336
724
  expect(mockOrchestrator.vmBashFullInternetAccess).toBe(true);
337
- expect(mockSetWebMcpMode).toHaveBeenCalledWith("native");
725
+ expect(mockSetWebMcpMode).toHaveBeenCalledWith("polyfill");
338
726
  expect(mockOrchestrator.contextCompressionEnabled).toBe(true);
339
- expect(mockOrchestrator.reasoningEffort).toBe("high");
727
+ expect(mockOrchestrator.reasoningEffort).toBe("medium");
340
728
  expect(mockOrchestrator.useProxy).toBe(true);
341
- expect(mockOrchestrator.proxyUrl).toBe("http://proxy");
729
+ expect(mockOrchestrator.proxyUrl).toBe("http://proxy.local");
730
+ expect(mockOrchestrator.gitProxyUrl).toBe("http://git-proxy.local");
731
+ expect(mockOrchestrator.taskServerEnabled).toBe(true);
732
+ expect(mockOrchestrator.taskServerUrl).toBe("http://tasks.local");
733
+ });
734
+
735
+ it("should handle default/fallback values for feature flags", async () => {
736
+ mockGetConfig.mockResolvedValue(undefined);
737
+
738
+ await initFeatureFlagsAndLimits(
739
+ mockOrchestrator as unknown as Orchestrator,
740
+ mockDb,
741
+ );
742
+
743
+ expect(mockOrchestrator.vmBootMode).toBe("disabled");
744
+ expect(mockOrchestrator.streamingEnabled).toBe(true);
745
+ expect(mockOrchestrator.webMcpToolsEnabled).toBe(true);
746
+ expect(mockOrchestrator.vmBashFullInternetAccess).toBe(false);
747
+ expect(mockOrchestrator.contextCompressionEnabled).toBe(false);
748
+ expect(mockOrchestrator.reasoningEffort).toBe("none");
749
+ expect(mockOrchestrator.useProxy).toBe(false);
750
+ expect(mockOrchestrator.proxyUrl).toBe("/proxy");
751
+ expect(mockOrchestrator.gitProxyUrl).toBe("/git-proxy");
752
+ expect(mockOrchestrator.taskServerEnabled).toBe(false);
753
+ expect(mockOrchestrator.taskServerUrl).toBe(DEFAULT_TASK_SERVER_URL);
754
+ });
755
+
756
+ it("should handle vmBootMode values auto, ext2, disabled, and invalid", async () => {
757
+ for (const mode of ["auto", "ext2", "disabled"]) {
758
+ mockGetConfig.mockResolvedValueOnce(mode);
759
+ await initFeatureFlagsAndLimits(
760
+ mockOrchestrator as unknown as Orchestrator,
761
+ mockDb,
762
+ );
763
+ expect(mockOrchestrator.vmBootMode).toBe(mode);
764
+ }
765
+
766
+ mockGetConfig.mockResolvedValueOnce("invalid-mode");
767
+ await initFeatureFlagsAndLimits(
768
+ mockOrchestrator as unknown as Orchestrator,
769
+ mockDb,
770
+ );
771
+ expect(mockOrchestrator.vmBootMode).toBe("disabled");
342
772
  });
343
773
  });
344
774
 
345
775
  describe("initLlamafileAndMesh", () => {
346
- it("should set llamafile modes", async () => {
347
- mockGetConfig.mockImplementation(async (_db: any, key: any) => {
348
- if (key === CONFIG_KEYS.LLAMAFILE_MODE) return "cli";
349
- if (key === CONFIG_KEYS.LLAMAFILE_HOST) return "http://localhost:8080";
350
- if (key === CONFIG_KEYS.LLAMAFILE_PORT) return "9090";
351
- if (key === CONFIG_KEYS.LLAMAFILE_OFFLINE) return "false";
352
- if (key === CONFIG_KEYS.MESH_LLM_HOST) return "mesh-host";
353
- return null;
776
+ it("should load llamafile server mode, host, port, and offline flag", async () => {
777
+ mockGetConfig.mockImplementation(async (_db, key) => {
778
+ switch (key) {
779
+ case CONFIG_KEYS.LLAMAFILE_MODE:
780
+ return "server";
781
+ case CONFIG_KEYS.LLAMAFILE_HOST:
782
+ return "http://llama.local";
783
+ case CONFIG_KEYS.LLAMAFILE_PORT:
784
+ return "8088";
785
+ case CONFIG_KEYS.LLAMAFILE_OFFLINE:
786
+ return "false";
787
+ case CONFIG_KEYS.MESH_LLM_HOST:
788
+ return "http://mesh.local";
789
+ default:
790
+ return undefined;
791
+ }
354
792
  });
355
793
 
356
- await initLlamafileAndMesh(mockOrchestrator, mockDb);
794
+ await initLlamafileAndMesh(
795
+ mockOrchestrator as unknown as Orchestrator,
796
+ mockDb,
797
+ );
357
798
 
358
- expect(mockOrchestrator.llamafileMode).toBe("cli");
359
- expect(mockOrchestrator.llamafileHost).toBe("http://localhost:8080");
360
- expect(mockOrchestrator.llamafilePort).toBe(9090);
799
+ expect(mockOrchestrator.llamafileMode).toBe("server");
800
+ expect(mockOrchestrator.llamafileHost).toBe("http://llama.local");
801
+ expect(mockOrchestrator.llamafilePort).toBe(8088);
361
802
  expect(mockOrchestrator.llamafileOffline).toBe(false);
362
- expect(mockOrchestrator.meshLlmHost).toBe("mesh-host");
803
+ expect(mockOrchestrator.meshLlmHost).toBe("http://mesh.local");
804
+ expect(mockApplyLlamafileHeaders).toHaveBeenCalledWith(
805
+ mockOrchestrator as unknown as Orchestrator,
806
+ );
807
+ expect(mockApplyMeshLlmHeaders).toHaveBeenCalledWith(
808
+ mockOrchestrator as unknown as Orchestrator,
809
+ );
810
+ });
811
+
812
+ it("should persist default llamafileMode when stored mode is invalid or null", async () => {
813
+ mockOrchestrator.llamafileMode = "cli";
814
+ mockGetConfig.mockImplementation(async (_db, key) => {
815
+ if (key === CONFIG_KEYS.LLAMAFILE_PORT) return "999999"; // out of bounds
816
+ return undefined;
817
+ });
818
+
819
+ await initLlamafileAndMesh(
820
+ mockOrchestrator as unknown as Orchestrator,
821
+ mockDb,
822
+ );
823
+
824
+ expect(mockSetConfig).toHaveBeenCalledWith(
825
+ mockDb,
826
+ CONFIG_KEYS.LLAMAFILE_MODE,
827
+ "cli",
828
+ );
829
+ expect(mockOrchestrator.llamafilePort).toBeUndefined();
830
+ });
831
+
832
+ it("should handle missing llamafile port", async () => {
833
+ mockGetConfig.mockImplementation(async (_db, key) => {
834
+ if (key === CONFIG_KEYS.LLAMAFILE_PORT) return undefined;
835
+ return undefined;
836
+ });
837
+
838
+ await initLlamafileAndMesh(
839
+ mockOrchestrator as unknown as Orchestrator,
840
+ mockDb,
841
+ );
842
+ expect(mockOrchestrator.llamafilePort).toBeUndefined();
363
843
  });
364
844
  });
365
845
 
366
846
  describe("initProviderAndModel", () => {
367
- it("should load provider and model config correctly", async () => {
368
- mockGetConfig.mockImplementation(async (_db: any, key: any) => {
369
- if (key === CONFIG_KEYS.PROVIDER) return "openrouter";
370
- if (key === CONFIG_KEYS.MODEL) return "gpt-4";
371
- if (key === CONFIG_KEYS.MAX_TOKENS) return "4096";
372
- if (key === CONFIG_KEYS.MAX_ITERATIONS) return "10";
373
- if (key === CONFIG_KEYS.RATE_LIMIT_CALLS_PER_MINUTE) return "20";
374
- if (key === CONFIG_KEYS.RATE_LIMIT_AUTO_ADAPT) return "false";
375
- return null;
847
+ it("should configure provider and model and schedule model info fetch", async () => {
848
+ mockGetConfig.mockImplementation(async (_db, key) => {
849
+ switch (key) {
850
+ case CONFIG_KEYS.PROVIDER:
851
+ return "openrouter";
852
+ case CONFIG_KEYS.MODEL:
853
+ return "openai/gpt-4o";
854
+ case CONFIG_KEYS.MAX_TOKENS:
855
+ return "2048";
856
+ case CONFIG_KEYS.MAX_ITERATIONS:
857
+ return "15";
858
+ case CONFIG_KEYS.RATE_LIMIT_CALLS_PER_MINUTE:
859
+ return "30";
860
+ case CONFIG_KEYS.RATE_LIMIT_AUTO_ADAPT:
861
+ return "false";
862
+ case CONFIG_KEYS.BEDROCK_REGION_FALLBACK:
863
+ return "us-east-1";
864
+ case CONFIG_KEYS.BEDROCK_PROFILE_FALLBACK:
865
+ return "my-profile";
866
+ case CONFIG_KEYS.BEDROCK_AUTH_MODE:
867
+ return "bearer";
868
+ default:
869
+ return undefined;
870
+ }
376
871
  });
377
872
 
378
- await initProviderAndModel(mockOrchestrator, mockDb);
379
-
380
- // fetchModelInfo is now fired as a background promise (fire-and-forget).
381
- // Flush the microtask queue so its .then() chain has a chance to run.
873
+ await initProviderAndModel(
874
+ mockOrchestrator as unknown as Orchestrator,
875
+ mockDb,
876
+ );
382
877
  await Promise.resolve();
383
878
 
384
879
  expect(mockOrchestrator.provider).toBe("openrouter");
385
- expect(mockOrchestrator.model).toBe("gpt-4");
386
- expect(mockOrchestrator.maxIterations).toBe(10);
387
- expect(mockOrchestrator.rateLimitCallsPerMinute).toBe(20);
880
+ expect(mockOrchestrator.model).toBe("openai/gpt-4o");
881
+ expect(mockOrchestrator.maxTokens).toBe(2048);
882
+ expect(mockOrchestrator.maxIterations).toBe(15);
883
+ expect(mockOrchestrator.rateLimitCallsPerMinute).toBe(30);
388
884
  expect(mockOrchestrator.rateLimitAutoAdapt).toBe(false);
885
+ expect(mockOrchestrator.bedrockRegionFallback).toBe("us-east-1");
886
+ expect(mockOrchestrator.bedrockProfileFallback).toBe("my-profile");
887
+ expect(mockOrchestrator.bedrockAuthMode).toBe("bearer");
389
888
  expect(mockFetchModelInfo).toHaveBeenCalled();
390
889
  });
391
- });
392
890
 
393
- describe("initWorkerAndScheduler", () => {
394
- it("should spawn worker and start scheduler", async () => {
395
- mockGetConfig.mockImplementation(async (_db: any, key: any) => {
396
- if (key === CONFIG_KEYS.STORAGE_HANDLE) return "handle-123";
397
- return null;
891
+ it("should handle prompt_api fallback model and token limits", async () => {
892
+ mockIsPromptApiSupported.mockReturnValue(false);
893
+ mockOrchestrator.provider = "prompt_api";
894
+ mockOrchestrator.model = "browser-built-in";
895
+
896
+ mockGetConfig.mockImplementation(async (_db, key) => {
897
+ switch (key) {
898
+ case CONFIG_KEYS.PROVIDER:
899
+ return "prompt_api";
900
+ case CONFIG_KEYS.MODEL:
901
+ return "browser-built-in";
902
+ case CONFIG_KEYS.MAX_TOKENS:
903
+ return "4096";
904
+ case CONFIG_KEYS.PROMPT_API_FALLBACK_MODEL:
905
+ return "fallback-model-x";
906
+ default:
907
+ return undefined;
908
+ }
398
909
  });
399
910
 
400
- await initWorkerAndScheduler(mockOrchestrator, mockDb);
911
+ await initProviderAndModel(
912
+ mockOrchestrator as unknown as Orchestrator,
913
+ mockDb,
914
+ );
915
+
916
+ expect(mockFetchTokenizerConfig).toHaveBeenCalledWith("fallback-model-x");
917
+ expect(mockOrchestrator.maxTokens).toBeGreaterThan(0);
918
+ });
919
+
920
+ it("should handle empty model, rejected tokenizer fetch, invalid iterations and rate limits", async () => {
921
+ mockIsPromptApiSupported.mockReturnValue(false);
922
+ mockFetchTokenizerConfig.mockRejectedValueOnce(
923
+ new Error("tokenizer fetch failed"),
924
+ );
925
+ mockGetApiKeyForHeaders.mockResolvedValueOnce(null);
926
+ mockOrchestrator.provider = "prompt_api";
927
+ mockOrchestrator.model = "";
928
+
929
+ mockGetConfig.mockImplementation(async (_db, key) => {
930
+ switch (key) {
931
+ case CONFIG_KEYS.PROVIDER:
932
+ return undefined;
933
+ case CONFIG_KEYS.MAX_ITERATIONS:
934
+ return "not-a-number";
935
+ case CONFIG_KEYS.RATE_LIMIT_CALLS_PER_MINUTE:
936
+ return "not-a-number";
937
+ case CONFIG_KEYS.PROMPT_API_FALLBACK_MODEL:
938
+ return undefined;
939
+ default:
940
+ return undefined;
941
+ }
942
+ });
401
943
 
402
- // shouldStartLocalScheduler() is now deferred as a void promise, so we
403
- // must flush the microtask queue before checking scheduler.start().
944
+ await initProviderAndModel(
945
+ mockOrchestrator as unknown as Orchestrator,
946
+ mockDb,
947
+ );
404
948
  await Promise.resolve();
405
949
 
406
- expect(mockOrchestrator.agentWorker).toBeDefined();
407
- expect(syncProxyConfigToServiceWorker).toHaveBeenCalled();
408
- expect(TaskScheduler).toHaveBeenCalled();
950
+ expect(mockOrchestrator.maxIterations).toBe(DEFAULT_MAX_ITERATIONS);
951
+ expect(mockOrchestrator.rateLimitCallsPerMinute).toBe(0);
952
+ expect(mockFetchTokenizerConfig).toHaveBeenCalledWith(
953
+ DEFAULT_PROMPT_API_FALLBACK_MODEL,
954
+ );
955
+ });
409
956
 
410
- const schedulerInstance = (TaskScheduler as jest.Mock).mock.results[0]
411
- .value;
412
- expect((schedulerInstance as any).start).toHaveBeenCalled();
413
- expect(mockSetupPushTaskListener).toHaveBeenCalledWith(
414
- mockOrchestrator,
957
+ it("should keep model unchanged when prompt_api is supported", async () => {
958
+ mockIsPromptApiSupported.mockReturnValueOnce(true);
959
+ mockOrchestrator.provider = "prompt_api";
960
+ mockOrchestrator.model = "browser-built-in";
961
+ mockGetConfig.mockImplementation(async (_db, key) => {
962
+ if (key === CONFIG_KEYS.PROVIDER) return "prompt_api";
963
+ if (key === CONFIG_KEYS.MODEL) return "browser-built-in";
964
+ return undefined;
965
+ });
966
+
967
+ await initProviderAndModel(
968
+ mockOrchestrator as unknown as Orchestrator,
415
969
  mockDb,
416
970
  );
971
+ expect(mockOrchestrator.model).toBe("browser-built-in");
972
+ });
973
+
974
+ it("should keep custom model when prompt_api is not supported but model is custom", async () => {
975
+ mockIsPromptApiSupported.mockReturnValueOnce(false);
976
+ mockOrchestrator.provider = "prompt_api";
977
+ mockOrchestrator.model = "custom-model";
978
+ mockGetConfig.mockImplementation(async (_db, key) => {
979
+ if (key === CONFIG_KEYS.PROVIDER) return "prompt_api";
980
+ if (key === CONFIG_KEYS.MODEL) return "custom-model";
981
+ return undefined;
982
+ });
983
+
984
+ await initProviderAndModel(
985
+ mockOrchestrator as unknown as Orchestrator,
986
+ mockDb,
987
+ );
988
+ expect(mockOrchestrator.model).toBe("custom-model");
989
+ });
417
990
 
418
- // Verify worker messaging
419
- expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalledWith({
991
+ it("should reset legacy 8192 maxTokens to dynamic max tokens", async () => {
992
+ mockGetConfig.mockImplementation(async (_db, key) => {
993
+ if (key === CONFIG_KEYS.MAX_TOKENS) return "8192";
994
+ return undefined;
995
+ });
996
+
997
+ await initProviderAndModel(
998
+ mockOrchestrator as unknown as Orchestrator,
999
+ mockDb,
1000
+ );
1001
+ expect(mockOrchestrator.maxTokens).toBe(
1002
+ getModelMaxTokens(mockOrchestrator.model),
1003
+ );
1004
+ });
1005
+
1006
+ it("should handle non-idleCallback environment and DOM load listener", async () => {
1007
+ const origIdle = (
1008
+ globalThis as unknown as { requestIdleCallback?: unknown }
1009
+ ).requestIdleCallback;
1010
+ delete (globalThis as unknown as { requestIdleCallback?: unknown })
1011
+ .requestIdleCallback;
1012
+
1013
+ const addEventListenerSpy = jest.spyOn(
1014
+ window as unknown as {
1015
+ addEventListener: (
1016
+ type: string,
1017
+ listener: () => void,
1018
+ options?: unknown,
1019
+ ) => void;
1020
+ },
1021
+ "addEventListener",
1022
+ );
1023
+ const originalReadyState = document.readyState;
1024
+ Object.defineProperty(document, "readyState", {
1025
+ value: "loading",
1026
+ configurable: true,
1027
+ });
1028
+
1029
+ await initProviderAndModel(
1030
+ mockOrchestrator as unknown as Orchestrator,
1031
+ mockDb,
1032
+ );
1033
+
1034
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
1035
+ "load",
1036
+ expect.any(Function),
1037
+ { once: true },
1038
+ );
1039
+ const loadCall = addEventListenerSpy.mock.calls.find(
1040
+ (call) => (call[0] as string) === "load",
1041
+ );
1042
+ const loadHandler = loadCall?.[1] as (() => void) | undefined;
1043
+ loadHandler?.();
1044
+
1045
+ Object.defineProperty(document, "readyState", {
1046
+ value: originalReadyState,
1047
+ configurable: true,
1048
+ });
1049
+ addEventListenerSpy.mockRestore();
1050
+ if (origIdle !== undefined) {
1051
+ (
1052
+ globalThis as unknown as { requestIdleCallback: unknown }
1053
+ ).requestIdleCallback = origIdle;
1054
+ }
1055
+ });
1056
+
1057
+ it("should use requestIdleCallback when available", async () => {
1058
+ const origIdle = (
1059
+ globalThis as unknown as { requestIdleCallback?: unknown }
1060
+ ).requestIdleCallback;
1061
+ const mockIdle = jest.fn((cb: () => void) => {
1062
+ cb();
1063
+ return 1;
1064
+ });
1065
+ (
1066
+ globalThis as unknown as { requestIdleCallback: unknown }
1067
+ ).requestIdleCallback = mockIdle;
1068
+
1069
+ await initProviderAndModel(
1070
+ mockOrchestrator as unknown as Orchestrator,
1071
+ mockDb,
1072
+ );
1073
+ expect(mockIdle).toHaveBeenCalled();
1074
+
1075
+ if (origIdle !== undefined) {
1076
+ (
1077
+ globalThis as unknown as { requestIdleCallback: unknown }
1078
+ ).requestIdleCallback = origIdle;
1079
+ } else {
1080
+ delete (globalThis as unknown as { requestIdleCallback?: unknown })
1081
+ .requestIdleCallback;
1082
+ }
1083
+ });
1084
+ });
1085
+
1086
+ describe("initWorkerAndScheduler", () => {
1087
+ it("should spawn worker, handle messages, errors, storage handle, and scheduler callbacks", async () => {
1088
+ mockGetConfig.mockImplementation(async (_db, key) => {
1089
+ if (key === CONFIG_KEYS.STORAGE_HANDLE) return "store-handle-xyz";
1090
+ return undefined;
1091
+ });
1092
+ mockShouldStartLocalScheduler.mockResolvedValueOnce(true);
1093
+
1094
+ await initWorkerAndScheduler(
1095
+ mockOrchestrator as unknown as Orchestrator,
1096
+ mockDb,
1097
+ );
1098
+ await Promise.resolve();
1099
+
1100
+ expect(mockOrchestrator.agentWorker).toBeDefined();
1101
+ const worker = mockOrchestrator.agentWorker!;
1102
+
1103
+ expect(worker.postMessage).toHaveBeenCalledWith({
420
1104
  type: "set-storage",
421
- payload: { storageHandle: "handle-123" },
1105
+ payload: { storageHandle: "store-handle-xyz" },
422
1106
  });
423
1107
 
424
- // Trigger worker message
425
- mockOrchestrator.agentWorker.onmessage({ data: { type: "test" } } as any);
1108
+ // worker.onmessage
1109
+ expect(worker.onmessage).not.toBeNull();
1110
+ worker.onmessage!({ data: { type: "test-msg" } });
426
1111
  expect(mockHandleWorkerMessage).toHaveBeenCalledWith(
427
- mockOrchestrator,
1112
+ mockOrchestrator as unknown as Orchestrator,
428
1113
  mockDb,
429
- { type: "test" },
1114
+ { type: "test-msg" },
430
1115
  );
431
1116
 
432
- // Trigger scheduler task
433
- const schedulerCb: any = (TaskScheduler as jest.Mock).mock.calls[0][0];
434
- await schedulerCb({ id: "task1" });
435
- expect(runTaskAsScheduled).toHaveBeenCalledWith(
436
- mockOrchestrator,
437
- expect.objectContaining({
438
- id: "task1",
439
- }),
1117
+ // worker.onerror
1118
+ const consoleErrorSpy = jest
1119
+ .spyOn(console, "error")
1120
+ .mockImplementation(() => {});
1121
+ expect(worker.onerror).not.toBeNull();
1122
+ worker.onerror!(new Error("Worker failure"));
1123
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1124
+ "Agent worker error:",
1125
+ expect.any(Error),
1126
+ );
1127
+ consoleErrorSpy.mockRestore();
1128
+
1129
+ // TaskScheduler start and callbacks
1130
+ expect(mockTaskSchedulerStart).toHaveBeenCalled();
1131
+ expect(capturedTaskExecutor).not.toBeNull();
1132
+ expect(capturedTaskOnExecuted).not.toBeNull();
1133
+
1134
+ const testTask: Task = {
1135
+ id: "task-1",
1136
+ name: "Test task",
1137
+ groupId: "g1",
1138
+ schedule: "0 * * * *",
1139
+ lastRun: null,
1140
+ prompt: "do work",
1141
+ enabled: true,
1142
+ createdAt: Date.now(),
1143
+ };
1144
+ await capturedTaskExecutor!(testTask);
1145
+ expect(mockRunTaskAsScheduled).toHaveBeenCalledWith(
1146
+ mockOrchestrator as unknown as Orchestrator,
1147
+ testTask,
440
1148
  );
1149
+
1150
+ capturedTaskOnExecuted!();
1151
+ expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("task-change", {
1152
+ type: "executed",
1153
+ });
1154
+
1155
+ expect(mockSetupPushTaskListener).toHaveBeenCalledWith(
1156
+ mockOrchestrator as unknown as Orchestrator,
1157
+ mockDb,
1158
+ );
1159
+ expect(mockSyncProxyConfigToServiceWorker).toHaveBeenCalledWith(
1160
+ mockOrchestrator as unknown as Orchestrator,
1161
+ );
1162
+ });
1163
+
1164
+ it("should not start scheduler when shouldStartLocalScheduler returns false and skip storage when absent", async () => {
1165
+ mockGetConfig.mockResolvedValueOnce(undefined); // no storage handle
1166
+ mockShouldStartLocalScheduler.mockResolvedValueOnce(false);
1167
+
1168
+ await initWorkerAndScheduler(
1169
+ mockOrchestrator as unknown as Orchestrator,
1170
+ mockDb,
1171
+ );
1172
+ await Promise.resolve();
1173
+
1174
+ expect(mockTaskSchedulerStart).not.toHaveBeenCalled();
1175
+ expect(mockOrchestrator.agentWorker?.postMessage).not.toHaveBeenCalled();
441
1176
  });
442
1177
  });
443
1178
  });