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.
- package/.well-known/mcp/server-card.json +2 -2
- package/.well-known/mcp/servers.json +1 -1
- package/.well-known/mcp.json +2 -2
- package/dist/cli/cli.js +2 -2
- package/dist/lib/components/index.js +2 -2
- package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
- package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
- package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
- package/dist/lib/index.js +2 -2
- package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
- package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
- package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
- package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
- package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
- package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
- package/dist/public/.well-known/mcp/server-card.json +2 -2
- package/dist/public/.well-known/mcp/servers.json +1 -1
- package/dist/public/.well-known/mcp.json +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/publishing/index.html +2 -2
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +2 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
- package/src/core/effect.test.ts +19 -0
- package/src/core/orchestrator/orchestrator-state.ts +2 -1
- package/src/core/orchestrator/orchestrator.test.ts +686 -5
- package/src/core/orchestrator/orchestrator.ts +3 -10
- package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
- package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
- package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
- package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
- package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
- package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
- package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
- package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
- package/src/core/orchestrator/utils/initTasks.ts +5 -4
- package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
- package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
- package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
- package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
- package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
- package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
- package/src/core/orchestrator/utils/operations/task.ts +1 -1
- package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
- package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
- package/src/core/orchestrator/utils/settings.test.ts +248 -81
- package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
- package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
- package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
- package/src/server/routes/mcp.ts +2 -2
- package/src/stores/file-viewer.test.ts +11 -0
- package/src/stores/theme.test.ts +41 -2
- package/src/stores/toast.test.ts +25 -0
- package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
- package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
- package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
- package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
- package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
- package/src/worker/tools/node-transformers-executor.test.ts +105 -6
- package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
- package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
- package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
- package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
- package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
- package/src/worker/tools/tasks/update-task.test.ts +78 -14
- package/src/worker/tools/ui/javascript.test.ts +70 -17
- package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
- package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
import type { ProviderConfig } from "../../../config/config.js";
|
|
3
|
+
import type { ShadowClawDatabase } from "../../../db/db.js";
|
|
4
|
+
import type { Orchestrator } from "../orchestrator.js";
|
|
2
5
|
|
|
3
6
|
jest.unstable_mockModule("./operations/channel.js", () => ({
|
|
4
7
|
applyAllChannelRunningStates: jest.fn(),
|
|
@@ -13,10 +16,12 @@ jest.unstable_mockModule("./operations/channel.js", () => ({
|
|
|
13
16
|
shouldRunChannel: jest.fn(),
|
|
14
17
|
}));
|
|
15
18
|
|
|
16
|
-
const mockDeliverResponse =
|
|
17
|
-
|
|
18
|
-
undefined
|
|
19
|
-
|
|
19
|
+
const mockDeliverResponse = jest
|
|
20
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
21
|
+
.mockResolvedValue(undefined);
|
|
22
|
+
const mockDeliverIntermediateResponse = jest
|
|
23
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
24
|
+
.mockResolvedValue(undefined);
|
|
20
25
|
|
|
21
26
|
jest.unstable_mockModule("./deliverResponse.js", () => ({
|
|
22
27
|
deliverResponse: mockDeliverResponse,
|
|
@@ -32,25 +37,33 @@ jest.unstable_mockModule("./operations/room.js", () => ({
|
|
|
32
37
|
listRooms: jest.fn(),
|
|
33
38
|
}));
|
|
34
39
|
|
|
35
|
-
const mockIsLlamafileResolutionError =
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
const mockIsLlamafileResolutionError =
|
|
41
|
+
jest.fn<(...args: unknown[]) => boolean>();
|
|
42
|
+
const mockDetectProviderHelpType =
|
|
43
|
+
jest.fn<(...args: unknown[]) => string | null>();
|
|
44
|
+
const mockIsTransformersJsResolutionError =
|
|
45
|
+
jest.fn<(...args: unknown[]) => boolean>();
|
|
46
|
+
|
|
47
|
+
const mockDeleteTask = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
48
|
+
const mockGetAllTasks = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
49
|
+
const mockRoomIdFromGroupId = jest.fn<(...args: unknown[]) => string>();
|
|
50
|
+
const mockSaveTask = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
51
|
+
const mockGetOrCreateSubscriberId =
|
|
52
|
+
jest.fn<(...args: unknown[]) => Promise<string>>();
|
|
53
|
+
|
|
54
|
+
const mockSyncTaskToServer =
|
|
55
|
+
jest.fn<(...args: unknown[]) => Promise<boolean>>();
|
|
56
|
+
const mockDeleteTaskFromServer =
|
|
57
|
+
jest.fn<(...args: unknown[]) => Promise<boolean>>();
|
|
58
|
+
|
|
59
|
+
const mockGetRemoteMcpConnection =
|
|
60
|
+
jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
61
|
+
const mockReconnectMcpOAuth =
|
|
62
|
+
jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
63
|
+
const mockGetPushUrl = jest.fn<(...args: unknown[]) => Promise<string>>();
|
|
64
|
+
const mockGetConfig = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
65
|
+
|
|
66
|
+
const mockShowToast = jest.fn<(...args: unknown[]) => unknown>();
|
|
54
67
|
|
|
55
68
|
jest.unstable_mockModule(
|
|
56
69
|
"../../../components/common/help/llamafile.js",
|
|
@@ -111,14 +124,14 @@ jest.unstable_mockModule("../../../db/rooms.js", () => ({
|
|
|
111
124
|
roomIdFromGroupId: mockRoomIdFromGroupId,
|
|
112
125
|
ROOM_PREFIX: "room:",
|
|
113
126
|
roomGroupId: (id: string) => `room:${id}`,
|
|
114
|
-
getRoomMetadata:
|
|
115
|
-
saveRoomMetadata:
|
|
116
|
-
getRoom:
|
|
117
|
-
upsertRoom:
|
|
118
|
-
createRoom:
|
|
119
|
-
addRoomMember:
|
|
120
|
-
removeRoomMember:
|
|
121
|
-
deleteRoom:
|
|
127
|
+
getRoomMetadata: jest.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
|
|
128
|
+
saveRoomMetadata: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
129
|
+
getRoom: jest.fn<() => Promise<null>>().mockResolvedValue(null),
|
|
130
|
+
upsertRoom: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
131
|
+
createRoom: jest.fn<() => Promise<unknown>>().mockResolvedValue({}),
|
|
132
|
+
addRoomMember: jest.fn<() => Promise<null>>().mockResolvedValue(null),
|
|
133
|
+
removeRoomMember: jest.fn<() => Promise<null>>().mockResolvedValue(null),
|
|
134
|
+
deleteRoom: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
122
135
|
}));
|
|
123
136
|
|
|
124
137
|
jest.unstable_mockModule("../../../db/saveTask.js", () => ({
|
|
@@ -129,10 +142,16 @@ jest.unstable_mockModule("../../../db/getOrCreateSubscriberId.js", () => ({
|
|
|
129
142
|
getOrCreateSubscriberId: mockGetOrCreateSubscriberId,
|
|
130
143
|
}));
|
|
131
144
|
|
|
145
|
+
interface MockOrchestratorStore {
|
|
146
|
+
runTask: jest.Mock;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const mockOrchestratorStore: MockOrchestratorStore = {
|
|
150
|
+
runTask: jest.fn(),
|
|
151
|
+
};
|
|
152
|
+
|
|
132
153
|
jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
|
|
133
|
-
orchestratorStore:
|
|
134
|
-
runTask: jest.fn(),
|
|
135
|
-
},
|
|
154
|
+
orchestratorStore: mockOrchestratorStore,
|
|
136
155
|
}));
|
|
137
156
|
|
|
138
157
|
jest.unstable_mockModule(
|
|
@@ -144,8 +163,8 @@ jest.unstable_mockModule(
|
|
|
144
163
|
);
|
|
145
164
|
|
|
146
165
|
const mockToolsStore = {
|
|
147
|
-
activateProfile: jest.fn(),
|
|
148
|
-
setToolEnabled: jest.fn(),
|
|
166
|
+
activateProfile: jest.fn<(...args: unknown[]) => Promise<void>>(),
|
|
167
|
+
setToolEnabled: jest.fn<(...args: unknown[]) => Promise<void>>(),
|
|
149
168
|
enabledTools: ["t1"],
|
|
150
169
|
systemPromptOverride: "override",
|
|
151
170
|
};
|
|
@@ -156,7 +175,9 @@ jest.unstable_mockModule("../../../stores/tools.js", () => ({
|
|
|
156
175
|
|
|
157
176
|
jest.unstable_mockModule("../../../subsystems/mcp/mcp-connections.js", () => ({
|
|
158
177
|
getRemoteMcpConnection: mockGetRemoteMcpConnection,
|
|
159
|
-
listRemoteMcpConnections:
|
|
178
|
+
listRemoteMcpConnections: jest
|
|
179
|
+
.fn<() => Promise<unknown[]>>()
|
|
180
|
+
.mockResolvedValue([]),
|
|
160
181
|
}));
|
|
161
182
|
|
|
162
183
|
jest.unstable_mockModule("../../../subsystems/mcp/mcp-reconnect.js", () => ({
|
|
@@ -178,9 +199,12 @@ jest.unstable_mockModule("../../../db/getConfig.js", () => ({
|
|
|
178
199
|
getConfig: mockGetConfig,
|
|
179
200
|
}));
|
|
180
201
|
|
|
181
|
-
const mockGetApiKeyForRequest =
|
|
182
|
-
|
|
183
|
-
const
|
|
202
|
+
const mockGetApiKeyForRequest =
|
|
203
|
+
jest.fn<(...args: unknown[]) => Promise<string | null>>();
|
|
204
|
+
const mockGetProviderRuntimeHeaders =
|
|
205
|
+
jest.fn<(...args: unknown[]) => Record<string, string>>();
|
|
206
|
+
const mockStopTransformersProgressPolling =
|
|
207
|
+
jest.fn<(...args: unknown[]) => unknown>();
|
|
184
208
|
|
|
185
209
|
jest.unstable_mockModule("./operations/provider.js", () => ({
|
|
186
210
|
getApiKeyForRequest: mockGetApiKeyForRequest,
|
|
@@ -191,26 +215,30 @@ jest.unstable_mockModule("./operations/provider.js", () => ({
|
|
|
191
215
|
startTransformersProgressPolling: jest.fn(),
|
|
192
216
|
}));
|
|
193
217
|
|
|
194
|
-
const mockBuildHeaders =
|
|
195
|
-
|
|
196
|
-
const
|
|
218
|
+
const mockBuildHeaders =
|
|
219
|
+
jest.fn<(...args: unknown[]) => Record<string, string>>();
|
|
220
|
+
const mockFormatRequest =
|
|
221
|
+
jest.fn<(...args: unknown[]) => Record<string, unknown>>();
|
|
222
|
+
const mockParseResponse =
|
|
223
|
+
jest.fn<(...args: unknown[]) => Record<string, unknown>>();
|
|
197
224
|
|
|
198
225
|
jest.unstable_mockModule("../../../subsystems/providers/providers.js", () => ({
|
|
199
226
|
buildHeaders: mockBuildHeaders,
|
|
200
227
|
formatRequest: mockFormatRequest,
|
|
201
228
|
getContextLimit: jest.fn().mockReturnValue(128000),
|
|
202
|
-
normalizeMeshLlmResult: jest.fn().mockImplementation((r
|
|
229
|
+
normalizeMeshLlmResult: jest.fn().mockImplementation((r) => r),
|
|
203
230
|
parseResponse: mockParseResponse,
|
|
204
231
|
}));
|
|
205
232
|
|
|
206
|
-
const mockSummarizeText = jest.fn()
|
|
207
|
-
const mockWriteText = jest.fn()
|
|
208
|
-
const mockRewriteText = jest.fn()
|
|
209
|
-
const mockProofreadText = jest.fn()
|
|
210
|
-
const mockDetectLanguage = jest.fn()
|
|
211
|
-
const mockTranslateText = jest.fn()
|
|
212
|
-
const mockEmbedText = jest.fn()
|
|
213
|
-
const mockEnsureBuiltinAiPolyfills =
|
|
233
|
+
const mockSummarizeText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
234
|
+
const mockWriteText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
235
|
+
const mockRewriteText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
236
|
+
const mockProofreadText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
237
|
+
const mockDetectLanguage = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
238
|
+
const mockTranslateText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
239
|
+
const mockEmbedText = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
240
|
+
const mockEnsureBuiltinAiPolyfills =
|
|
241
|
+
jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
214
242
|
|
|
215
243
|
jest.unstable_mockModule(
|
|
216
244
|
"../../../subsystems/providers/builtin-ai-tasks.js",
|
|
@@ -223,10 +251,12 @@ jest.unstable_mockModule(
|
|
|
223
251
|
summarizeText: mockSummarizeText,
|
|
224
252
|
translateText: mockTranslateText,
|
|
225
253
|
writeText: mockWriteText,
|
|
226
|
-
createTaskInstanceWithFallback:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
254
|
+
createTaskInstanceWithFallback: jest
|
|
255
|
+
.fn<() => Promise<unknown>>()
|
|
256
|
+
.mockResolvedValue({}),
|
|
257
|
+
getPromptApiFallbackModel: jest
|
|
258
|
+
.fn<() => Promise<string>>()
|
|
259
|
+
.mockResolvedValue("onnx-community/Qwen3-0.6B-ONNX"),
|
|
230
260
|
PROMPT_API_POLYFILL_MODEL: "onnx-community/Qwen3-0.6B-ONNX",
|
|
231
261
|
}),
|
|
232
262
|
);
|
|
@@ -235,28 +265,58 @@ const { handleWorkerMessage } = await import("./handleWorkerMessage.js");
|
|
|
235
265
|
const { createRoom, inviteToRoom, leaveRoom } =
|
|
236
266
|
await import("./operations/room.js");
|
|
237
267
|
|
|
268
|
+
interface PendingResolver {
|
|
269
|
+
resolve: (value: unknown) => void;
|
|
270
|
+
reject: (reason: unknown) => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const globalNativeAi = globalThis as typeof globalThis & {
|
|
274
|
+
pendingNativeAiResolvers?: Record<string, PendingResolver>;
|
|
275
|
+
LanguageModel?: {
|
|
276
|
+
create: () => Promise<{
|
|
277
|
+
prompt: (p?: string) => Promise<string>;
|
|
278
|
+
destroy: () => void;
|
|
279
|
+
}>;
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const dummyProviderConfig: ProviderConfig = {
|
|
284
|
+
id: "test-prov",
|
|
285
|
+
name: "Test Provider",
|
|
286
|
+
baseUrl: "https://api.test.example/v1",
|
|
287
|
+
format: "openai",
|
|
288
|
+
requiresApiKey: true,
|
|
289
|
+
apiKeyHeader: "Authorization",
|
|
290
|
+
headers: {},
|
|
291
|
+
supportsStreaming: true,
|
|
292
|
+
defaultModel: "test-model",
|
|
293
|
+
};
|
|
294
|
+
|
|
238
295
|
describe("handleWorkerMessage", () => {
|
|
239
|
-
let mockOrchestrator:
|
|
240
|
-
let mockDb:
|
|
296
|
+
let mockOrchestrator: Orchestrator;
|
|
297
|
+
let mockDb: ShadowClawDatabase;
|
|
241
298
|
|
|
242
299
|
beforeEach(() => {
|
|
243
300
|
jest.clearAllMocks();
|
|
244
301
|
mockSyncTaskToServer.mockResolvedValue(true);
|
|
245
302
|
mockDeleteTaskFromServer.mockResolvedValue(true);
|
|
246
303
|
mockGetOrCreateSubscriberId.mockResolvedValue("sub-test");
|
|
247
|
-
|
|
304
|
+
|
|
305
|
+
mockDb = {} as unknown as ShadowClawDatabase;
|
|
248
306
|
mockOrchestrator = {
|
|
249
|
-
transformersProgressPollers: new Map(),
|
|
307
|
+
transformersProgressPollers: new Map<string, unknown>(),
|
|
250
308
|
clearProviderRequest: jest.fn(),
|
|
251
|
-
inFlightTriggerByGroup: new Map(),
|
|
252
|
-
inFlightEffectiveProviderByGroup: new Map(),
|
|
253
|
-
deliverResponse:
|
|
254
|
-
|
|
255
|
-
undefined,
|
|
256
|
-
|
|
309
|
+
inFlightTriggerByGroup: new Map<string, string>(),
|
|
310
|
+
inFlightEffectiveProviderByGroup: new Map<string, unknown>(),
|
|
311
|
+
deliverResponse: jest
|
|
312
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
313
|
+
.mockResolvedValue(undefined),
|
|
314
|
+
deliverIntermediateResponse: jest
|
|
315
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
316
|
+
.mockResolvedValue(undefined),
|
|
257
317
|
setState: jest.fn(),
|
|
258
318
|
events: { emit: jest.fn() },
|
|
259
|
-
schedulerTriggeredGroups: new Set(),
|
|
319
|
+
schedulerTriggeredGroups: new Set<string>(),
|
|
260
320
|
createRoom: jest.fn(),
|
|
261
321
|
inviteToRoom: jest.fn(),
|
|
262
322
|
leaveRoom: jest.fn(),
|
|
@@ -264,14 +324,23 @@ describe("handleWorkerMessage", () => {
|
|
|
264
324
|
providerConfig: { requiresApiKey: true },
|
|
265
325
|
router: {
|
|
266
326
|
setTyping: jest.fn(),
|
|
267
|
-
send:
|
|
327
|
+
send: jest
|
|
328
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
329
|
+
.mockResolvedValue(undefined),
|
|
268
330
|
findChannel: jest.fn(),
|
|
269
331
|
},
|
|
270
|
-
newSession:
|
|
271
|
-
|
|
332
|
+
newSession: jest
|
|
333
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
334
|
+
.mockResolvedValue(undefined),
|
|
335
|
+
handleCompactDone: jest
|
|
336
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
337
|
+
.mockResolvedValue(undefined),
|
|
272
338
|
agentWorker: { postMessage: jest.fn() },
|
|
273
339
|
roomManager: { broadcastA2UI: jest.fn() },
|
|
274
|
-
|
|
340
|
+
maxTokens: 1000,
|
|
341
|
+
provider: "test-provider",
|
|
342
|
+
model: "test-model",
|
|
343
|
+
} as unknown as Orchestrator;
|
|
275
344
|
|
|
276
345
|
mockGetPushUrl.mockResolvedValue("http://push");
|
|
277
346
|
mockGetAllTasks.mockResolvedValue([]);
|
|
@@ -283,11 +352,17 @@ describe("handleWorkerMessage", () => {
|
|
|
283
352
|
mockParseResponse.mockReturnValue({
|
|
284
353
|
content: [{ type: "text", text: "parsed result" }],
|
|
285
354
|
});
|
|
286
|
-
global.fetch =
|
|
355
|
+
global.fetch = jest
|
|
356
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
357
|
+
.mockResolvedValue({} as unknown as Response) as typeof global.fetch;
|
|
287
358
|
});
|
|
288
359
|
|
|
289
|
-
const send = async (msg:
|
|
290
|
-
handleWorkerMessage(
|
|
360
|
+
const send = async (msg: { type: string; payload?: unknown }) =>
|
|
361
|
+
handleWorkerMessage(
|
|
362
|
+
mockOrchestrator,
|
|
363
|
+
mockDb,
|
|
364
|
+
msg as Record<string, unknown>,
|
|
365
|
+
);
|
|
291
366
|
|
|
292
367
|
it("handles response", async () => {
|
|
293
368
|
mockOrchestrator.inFlightTriggerByGroup.set("g1", "x");
|
|
@@ -344,15 +419,21 @@ describe("handleWorkerMessage", () => {
|
|
|
344
419
|
);
|
|
345
420
|
});
|
|
346
421
|
|
|
347
|
-
it("handles
|
|
348
|
-
|
|
422
|
+
it("handles run-task", async () => {
|
|
423
|
+
await send({ type: "run-task", payload: { task: { id: "t1" } } });
|
|
424
|
+
expect(mockOrchestratorStore.runTask).toHaveBeenCalledWith(
|
|
425
|
+
{ id: "t1" },
|
|
426
|
+
true,
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("handles task-created success and failures", async () => {
|
|
349
431
|
mockOrchestrator.schedulerTriggeredGroups.add("g1");
|
|
350
432
|
await send({ type: "task-created", payload: { task: { groupId: "g1" } } });
|
|
351
433
|
expect(mockShowToast).toHaveBeenCalledWith(
|
|
352
434
|
expect.stringContaining("blocked"),
|
|
353
435
|
expect.any(Object),
|
|
354
436
|
);
|
|
355
|
-
|
|
356
437
|
mockOrchestrator.schedulerTriggeredGroups.clear();
|
|
357
438
|
|
|
358
439
|
await send({ type: "task-created", payload: { task: { groupId: "g2" } } });
|
|
@@ -363,14 +444,64 @@ describe("handleWorkerMessage", () => {
|
|
|
363
444
|
"sub-test",
|
|
364
445
|
);
|
|
365
446
|
|
|
447
|
+
// Sync to server failed
|
|
448
|
+
mockSyncTaskToServer.mockResolvedValueOnce(false);
|
|
449
|
+
await send({ type: "task-created", payload: { task: { groupId: "g2" } } });
|
|
450
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
451
|
+
"Failed to sync task to server — task was not saved.",
|
|
452
|
+
{ type: "error" },
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Save task throws error
|
|
456
|
+
mockSaveTask.mockRejectedValueOnce(new Error("Save failed"));
|
|
457
|
+
await send({ type: "task-created", payload: { task: { groupId: "g2" } } });
|
|
458
|
+
expect(mockShowToast).toHaveBeenCalledWith("Failed to save task.", {
|
|
459
|
+
type: "error",
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("handles update-task success and failures", async () => {
|
|
464
|
+
mockOrchestrator.schedulerTriggeredGroups.add("g1");
|
|
465
|
+
await send({ type: "update-task", payload: { task: { groupId: "g1" } } });
|
|
466
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
467
|
+
expect.stringContaining("Task update blocked"),
|
|
468
|
+
expect.any(Object),
|
|
469
|
+
);
|
|
470
|
+
mockOrchestrator.schedulerTriggeredGroups.clear();
|
|
471
|
+
|
|
366
472
|
await send({ type: "update-task", payload: { task: { groupId: "g2" } } });
|
|
367
|
-
expect(mockSaveTask).
|
|
473
|
+
expect(mockSaveTask).toHaveBeenCalled();
|
|
368
474
|
expect(mockSyncTaskToServer).toHaveBeenLastCalledWith(
|
|
369
475
|
mockOrchestrator,
|
|
370
476
|
{ groupId: "g2" },
|
|
371
477
|
"sub-test",
|
|
372
478
|
);
|
|
373
479
|
|
|
480
|
+
// Sync to server failed
|
|
481
|
+
mockSyncTaskToServer.mockResolvedValueOnce(false);
|
|
482
|
+
await send({ type: "update-task", payload: { task: { groupId: "g2" } } });
|
|
483
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
484
|
+
"Failed to sync task update to server — task was not updated.",
|
|
485
|
+
{ type: "error" },
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Update throws error
|
|
489
|
+
mockSaveTask.mockRejectedValueOnce(new Error("Update failed"));
|
|
490
|
+
await send({ type: "update-task", payload: { task: { groupId: "g2" } } });
|
|
491
|
+
expect(mockShowToast).toHaveBeenCalledWith("Failed to update task.", {
|
|
492
|
+
type: "error",
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("handles delete-task success and failures", async () => {
|
|
497
|
+
mockOrchestrator.schedulerTriggeredGroups.add("g1");
|
|
498
|
+
await send({ type: "delete-task", payload: { id: "t1", groupId: "g1" } });
|
|
499
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
500
|
+
expect.stringContaining("Task deletion blocked"),
|
|
501
|
+
expect.any(Object),
|
|
502
|
+
);
|
|
503
|
+
mockOrchestrator.schedulerTriggeredGroups.clear();
|
|
504
|
+
|
|
374
505
|
await send({ type: "delete-task", payload: { id: "t1", groupId: "g2" } });
|
|
375
506
|
expect(mockDeleteTask).toHaveBeenCalled();
|
|
376
507
|
expect(mockDeleteTaskFromServer).toHaveBeenCalledWith(
|
|
@@ -378,15 +509,50 @@ describe("handleWorkerMessage", () => {
|
|
|
378
509
|
"t1",
|
|
379
510
|
"sub-test",
|
|
380
511
|
);
|
|
512
|
+
|
|
513
|
+
// Server deletion failed
|
|
514
|
+
mockDeleteTaskFromServer.mockResolvedValueOnce(false);
|
|
515
|
+
await send({ type: "delete-task", payload: { id: "t1", groupId: "g2" } });
|
|
516
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
517
|
+
"Failed to delete task from server — task kept in view.",
|
|
518
|
+
{ type: "error" },
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// Delete task throws
|
|
522
|
+
mockDeleteTask.mockRejectedValueOnce(new Error("Delete failed"));
|
|
523
|
+
await send({ type: "delete-task", payload: { id: "t1", groupId: "g2" } });
|
|
381
524
|
});
|
|
382
525
|
|
|
383
|
-
it("handles
|
|
526
|
+
it("handles task-list-request filtering by groupId", async () => {
|
|
527
|
+
mockGetAllTasks.mockResolvedValue([
|
|
528
|
+
{ id: "t1", groupId: "g1" },
|
|
529
|
+
{ id: "t2", groupId: "g2" },
|
|
530
|
+
]);
|
|
531
|
+
|
|
532
|
+
await send({ type: "task-list-request", payload: { groupId: "g1" } });
|
|
533
|
+
|
|
534
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
535
|
+
type: "task-list-response",
|
|
536
|
+
payload: {
|
|
537
|
+
groupId: "g1",
|
|
538
|
+
tasks: [{ id: "t1", groupId: "g1" }],
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("handles room actions and edge cases", async () => {
|
|
384
544
|
await send({
|
|
385
545
|
type: "room-action",
|
|
386
546
|
payload: { action: "create", name: "r1" },
|
|
387
547
|
});
|
|
388
548
|
expect(createRoom).toHaveBeenCalledWith(mockOrchestrator, "r1");
|
|
389
549
|
|
|
550
|
+
await send({
|
|
551
|
+
type: "room-action",
|
|
552
|
+
payload: { action: "create" },
|
|
553
|
+
});
|
|
554
|
+
expect(createRoom).toHaveBeenCalledWith(mockOrchestrator, "");
|
|
555
|
+
|
|
390
556
|
await send({
|
|
391
557
|
type: "room-action",
|
|
392
558
|
payload: { action: "invite", roomId: "r1", peerId: "p1" },
|
|
@@ -398,9 +564,24 @@ describe("handleWorkerMessage", () => {
|
|
|
398
564
|
payload: { action: "leave", roomId: "r1" },
|
|
399
565
|
});
|
|
400
566
|
expect(leaveRoom).toHaveBeenCalledWith(mockOrchestrator, "r1");
|
|
567
|
+
|
|
568
|
+
// Unknown action does not crash
|
|
569
|
+
await send({
|
|
570
|
+
type: "room-action",
|
|
571
|
+
payload: { action: "unknown" },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Thrown error is caught cleanly
|
|
575
|
+
(leaveRoom as jest.Mock).mockImplementationOnce(() => {
|
|
576
|
+
throw new Error("Leave error");
|
|
577
|
+
});
|
|
578
|
+
await send({
|
|
579
|
+
type: "room-action",
|
|
580
|
+
payload: { action: "leave", roomId: "r1" },
|
|
581
|
+
});
|
|
401
582
|
});
|
|
402
583
|
|
|
403
|
-
it("handles errors", async () => {
|
|
584
|
+
it("handles errors and context limit warnings", async () => {
|
|
404
585
|
await send({
|
|
405
586
|
type: "error",
|
|
406
587
|
payload: { groupId: "g1", error: "tokens_limit_reached" },
|
|
@@ -411,12 +592,25 @@ describe("handleWorkerMessage", () => {
|
|
|
411
592
|
"g1",
|
|
412
593
|
expect.stringContaining("context window"),
|
|
413
594
|
);
|
|
595
|
+
|
|
596
|
+
// Non-context error
|
|
597
|
+
await send({
|
|
598
|
+
type: "error",
|
|
599
|
+
payload: { groupId: "g1", error: "Unrelated provider fault" },
|
|
600
|
+
});
|
|
601
|
+
expect(mockDeliverResponse).toHaveBeenCalledWith(
|
|
602
|
+
mockOrchestrator,
|
|
603
|
+
mockDb,
|
|
604
|
+
"g1",
|
|
605
|
+
"⚠️ Error: Unrelated provider fault",
|
|
606
|
+
);
|
|
414
607
|
});
|
|
415
608
|
|
|
416
|
-
it("handles llamafile error", async () => {
|
|
609
|
+
it("handles llamafile error and transformers_js_local error", async () => {
|
|
417
610
|
mockOrchestrator.inFlightEffectiveProviderByGroup.set("g1", {
|
|
418
611
|
providerId: "llamafile",
|
|
419
612
|
model: "llamafile-model",
|
|
613
|
+
providerConfig: dummyProviderConfig,
|
|
420
614
|
});
|
|
421
615
|
mockIsLlamafileResolutionError.mockReturnValue(true);
|
|
422
616
|
await send({
|
|
@@ -427,12 +621,50 @@ describe("handleWorkerMessage", () => {
|
|
|
427
621
|
"provider-help",
|
|
428
622
|
expect.objectContaining({ providerId: "llamafile" }),
|
|
429
623
|
);
|
|
624
|
+
|
|
625
|
+
mockOrchestrator.inFlightEffectiveProviderByGroup.set("g2", {
|
|
626
|
+
providerId: "transformers_js_local",
|
|
627
|
+
model: "local-model",
|
|
628
|
+
providerConfig: dummyProviderConfig,
|
|
629
|
+
});
|
|
630
|
+
mockIsTransformersJsResolutionError.mockReturnValue(true);
|
|
631
|
+
await send({
|
|
632
|
+
type: "error",
|
|
633
|
+
payload: { groupId: "g2", error: "transformers local error" },
|
|
634
|
+
});
|
|
635
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("provider-help", {
|
|
636
|
+
providerId: "transformers_js_local",
|
|
637
|
+
reason: "transformers local error",
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("handles generic provider help detection", async () => {
|
|
642
|
+
mockOrchestrator.inFlightEffectiveProviderByGroup.set("g1", {
|
|
643
|
+
providerId: "openai",
|
|
644
|
+
model: "gpt-4",
|
|
645
|
+
providerConfig: dummyProviderConfig,
|
|
646
|
+
});
|
|
647
|
+
mockIsLlamafileResolutionError.mockReturnValue(false);
|
|
648
|
+
mockIsTransformersJsResolutionError.mockReturnValue(false);
|
|
649
|
+
mockDetectProviderHelpType.mockReturnValue("api_key_missing");
|
|
650
|
+
|
|
651
|
+
await send({
|
|
652
|
+
type: "error",
|
|
653
|
+
payload: { groupId: "g1", error: "Invalid API key" },
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("provider-help", {
|
|
657
|
+
providerId: "openai",
|
|
658
|
+
reason: "Invalid API key",
|
|
659
|
+
helpType: "api_key_missing",
|
|
660
|
+
});
|
|
430
661
|
});
|
|
431
662
|
|
|
432
|
-
it("handles simple events", async () => {
|
|
663
|
+
it("handles simple events and tool activities", async () => {
|
|
433
664
|
await send({ type: "typing", payload: { groupId: "g1" } });
|
|
434
|
-
expect(mockOrchestrator.router
|
|
665
|
+
expect(mockOrchestrator.router?.setTyping).toHaveBeenCalledWith("g1", true);
|
|
435
666
|
|
|
667
|
+
// write_file done
|
|
436
668
|
await send({
|
|
437
669
|
type: "tool-activity",
|
|
438
670
|
payload: { groupId: "g1", tool: "write_file", status: "done" },
|
|
@@ -441,6 +673,26 @@ describe("handleWorkerMessage", () => {
|
|
|
441
673
|
groupId: "g1",
|
|
442
674
|
});
|
|
443
675
|
|
|
676
|
+
// bash done
|
|
677
|
+
await send({
|
|
678
|
+
type: "tool-activity",
|
|
679
|
+
payload: { groupId: "g1", tool: "bash", status: "done" },
|
|
680
|
+
});
|
|
681
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("file-change", {
|
|
682
|
+
groupId: "g1",
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// other tool / running status
|
|
686
|
+
(mockOrchestrator.events.emit as jest.Mock).mockClear();
|
|
687
|
+
await send({
|
|
688
|
+
type: "tool-activity",
|
|
689
|
+
payload: { groupId: "g1", tool: "read_file", status: "done" },
|
|
690
|
+
});
|
|
691
|
+
expect(mockOrchestrator.events.emit).not.toHaveBeenCalledWith(
|
|
692
|
+
"file-change",
|
|
693
|
+
expect.anything(),
|
|
694
|
+
);
|
|
695
|
+
|
|
444
696
|
await send({
|
|
445
697
|
type: "compact-done",
|
|
446
698
|
payload: { groupId: "g1", summary: "s" },
|
|
@@ -451,56 +703,156 @@ describe("handleWorkerMessage", () => {
|
|
|
451
703
|
"s",
|
|
452
704
|
);
|
|
453
705
|
|
|
454
|
-
await send({ type: "task-list-request", payload: { groupId: "g1" } });
|
|
455
|
-
expect(mockOrchestrator.agentWorker.postMessage).toHaveBeenCalled();
|
|
456
|
-
|
|
457
706
|
await send({ type: "clear-chat", payload: { groupId: "g1" } });
|
|
458
707
|
expect(mockOrchestrator.newSession).toHaveBeenCalledWith(mockDb, "g1");
|
|
459
708
|
|
|
709
|
+
// Clear chat throws
|
|
710
|
+
(
|
|
711
|
+
mockOrchestrator.newSession as jest.Mock<
|
|
712
|
+
(...args: unknown[]) => Promise<unknown>
|
|
713
|
+
>
|
|
714
|
+
).mockRejectedValueOnce(new Error("Clear error"));
|
|
715
|
+
await send({ type: "clear-chat", payload: { groupId: "g1" } });
|
|
716
|
+
|
|
460
717
|
await send({ type: "show-toast", payload: { message: "msg" } });
|
|
461
|
-
expect(mockShowToast).
|
|
718
|
+
expect(mockShowToast).toHaveBeenCalledWith("msg", {
|
|
719
|
+
type: "info",
|
|
720
|
+
duration: undefined,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
await send({
|
|
724
|
+
type: "model-download-progress",
|
|
725
|
+
payload: { progress: 0.5 },
|
|
726
|
+
});
|
|
727
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith(
|
|
728
|
+
"model-download-progress",
|
|
729
|
+
{ progress: 0.5 },
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
await send({ type: "thinking-log", payload: { text: "thinking..." } });
|
|
733
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("thinking-log", {
|
|
734
|
+
text: "thinking...",
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
await send({ type: "token-usage", payload: { tokens: 100 } });
|
|
738
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith("token-usage", {
|
|
739
|
+
tokens: 100,
|
|
740
|
+
});
|
|
462
741
|
});
|
|
463
742
|
|
|
464
|
-
it("handles manage-tools", async () => {
|
|
743
|
+
it("handles manage-tools with enable, disable, and default groupId", async () => {
|
|
465
744
|
await send({
|
|
466
745
|
type: "manage-tools",
|
|
467
746
|
payload: { action: "activate_profile", profileId: "p1" },
|
|
468
747
|
});
|
|
469
748
|
expect(mockToolsStore.activateProfile).toHaveBeenCalledWith(mockDb, "p1");
|
|
749
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith(
|
|
750
|
+
expect.objectContaining({
|
|
751
|
+
payload: expect.objectContaining({
|
|
752
|
+
groupId: "br:main",
|
|
753
|
+
}),
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
470
756
|
|
|
471
757
|
await send({
|
|
472
758
|
type: "manage-tools",
|
|
473
|
-
payload: { action: "enable", toolNames: ["t1"] },
|
|
759
|
+
payload: { action: "enable", toolNames: ["t1"], groupId: "g1" },
|
|
474
760
|
});
|
|
475
761
|
expect(mockToolsStore.setToolEnabled).toHaveBeenCalledWith(
|
|
476
762
|
mockDb,
|
|
477
763
|
"t1",
|
|
478
764
|
true,
|
|
479
765
|
);
|
|
766
|
+
|
|
767
|
+
await send({
|
|
768
|
+
type: "manage-tools",
|
|
769
|
+
payload: { action: "disable", toolNames: ["t2"] },
|
|
770
|
+
});
|
|
771
|
+
expect(mockToolsStore.setToolEnabled).toHaveBeenCalledWith(
|
|
772
|
+
mockDb,
|
|
773
|
+
"t2",
|
|
774
|
+
false,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Unrecognized action
|
|
778
|
+
await send({
|
|
779
|
+
type: "manage-tools",
|
|
780
|
+
payload: { action: "unknown_action" },
|
|
781
|
+
});
|
|
480
782
|
});
|
|
481
783
|
|
|
482
|
-
it("handles push
|
|
784
|
+
it("handles push notifications and recursion guard", async () => {
|
|
785
|
+
mockOrchestrator.schedulerTriggeredGroups.add("g1");
|
|
786
|
+
await send({
|
|
787
|
+
type: "send-notification",
|
|
788
|
+
payload: { title: "t", body: "b", groupId: "g1" },
|
|
789
|
+
});
|
|
790
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
791
|
+
expect.stringContaining("Notification blocked"),
|
|
792
|
+
expect.any(Object),
|
|
793
|
+
);
|
|
794
|
+
mockOrchestrator.schedulerTriggeredGroups.clear();
|
|
795
|
+
|
|
483
796
|
await send({
|
|
484
797
|
type: "send-notification",
|
|
485
798
|
payload: { title: "t", body: "b" },
|
|
486
799
|
});
|
|
487
800
|
expect(mockGetPushUrl).toHaveBeenCalled();
|
|
801
|
+
|
|
802
|
+
// Fetch error handling
|
|
803
|
+
(
|
|
804
|
+
global.fetch as jest.Mock<(...args: unknown[]) => Promise<unknown>>
|
|
805
|
+
).mockRejectedValueOnce(new Error("Push network error"));
|
|
806
|
+
await send({
|
|
807
|
+
type: "send-notification",
|
|
808
|
+
payload: { title: "t", body: "b" },
|
|
809
|
+
});
|
|
810
|
+
await new Promise(process.nextTick);
|
|
488
811
|
});
|
|
489
812
|
|
|
490
|
-
it("handles send-file", async () => {
|
|
813
|
+
it("handles send-file variations", async () => {
|
|
491
814
|
await send({
|
|
492
815
|
type: "send-file",
|
|
493
816
|
payload: { groupId: "g1", path: "test.txt" },
|
|
494
817
|
});
|
|
495
|
-
// Need a tick since it's fire-and-forget IIFE
|
|
496
818
|
await new Promise(process.nextTick);
|
|
497
|
-
expect(mockOrchestrator.router
|
|
498
|
-
|
|
819
|
+
expect(mockOrchestrator.router?.send).toHaveBeenCalledWith("g1", "", [
|
|
820
|
+
expect.objectContaining({ fileName: "test.txt" }),
|
|
821
|
+
]);
|
|
499
822
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
823
|
+
// Empty path fallback
|
|
824
|
+
await send({
|
|
825
|
+
type: "send-file",
|
|
826
|
+
payload: { groupId: "g1", path: "" },
|
|
827
|
+
});
|
|
828
|
+
await new Promise(process.nextTick);
|
|
829
|
+
expect(mockOrchestrator.router?.send).toHaveBeenCalledWith("g1", "", [
|
|
830
|
+
expect.objectContaining({ fileName: "" }),
|
|
831
|
+
]);
|
|
832
|
+
|
|
833
|
+
// Rejection with non-Error
|
|
834
|
+
(
|
|
835
|
+
mockOrchestrator.router?.send as jest.Mock<
|
|
836
|
+
(...args: unknown[]) => Promise<unknown>
|
|
837
|
+
>
|
|
838
|
+
).mockRejectedValueOnce("raw string rejection");
|
|
839
|
+
await send({
|
|
840
|
+
type: "send-file",
|
|
841
|
+
payload: { groupId: "g1", path: "fail.txt" },
|
|
842
|
+
});
|
|
843
|
+
await new Promise(process.nextTick);
|
|
844
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
845
|
+
"Failed to send file to peer: raw string rejection",
|
|
846
|
+
expect.any(Object),
|
|
503
847
|
);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it("handles send-file rejection with Error", async () => {
|
|
851
|
+
(
|
|
852
|
+
mockOrchestrator.router?.send as jest.Mock<
|
|
853
|
+
(...args: unknown[]) => Promise<unknown>
|
|
854
|
+
>
|
|
855
|
+
).mockRejectedValueOnce(new Error("Send failed"));
|
|
504
856
|
const consoleError = jest
|
|
505
857
|
.spyOn(console, "error")
|
|
506
858
|
.mockImplementation(() => {});
|
|
@@ -530,8 +882,8 @@ describe("handleWorkerMessage", () => {
|
|
|
530
882
|
});
|
|
531
883
|
|
|
532
884
|
it("handles render-component", async () => {
|
|
533
|
-
mockOrchestrator.router
|
|
534
|
-
sendA2UI:
|
|
885
|
+
(mockOrchestrator.router?.findChannel as jest.Mock).mockReturnValue({
|
|
886
|
+
sendA2UI: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
535
887
|
});
|
|
536
888
|
mockRoomIdFromGroupId.mockReturnValue("r1");
|
|
537
889
|
|
|
@@ -549,11 +901,20 @@ describe("handleWorkerMessage", () => {
|
|
|
549
901
|
expect.any(Object),
|
|
550
902
|
);
|
|
551
903
|
expect(mockOrchestrator.roomManager.broadcastA2UI).toHaveBeenCalled();
|
|
904
|
+
|
|
905
|
+
// peer: group where channel does not implement sendA2UI
|
|
906
|
+
(mockOrchestrator.router?.findChannel as jest.Mock).mockReturnValue({});
|
|
907
|
+
await send({
|
|
908
|
+
type: "render-component",
|
|
909
|
+
payload: { groupId: "peer:g2", envelope: {} },
|
|
910
|
+
});
|
|
552
911
|
});
|
|
553
912
|
|
|
554
913
|
it("handles render-component sendA2UI rejection", async () => {
|
|
555
|
-
mockOrchestrator.router
|
|
556
|
-
sendA2UI:
|
|
914
|
+
(mockOrchestrator.router?.findChannel as jest.Mock).mockReturnValue({
|
|
915
|
+
sendA2UI: jest
|
|
916
|
+
.fn<() => Promise<void>>()
|
|
917
|
+
.mockRejectedValue(new Error("err")),
|
|
557
918
|
});
|
|
558
919
|
const consoleError = jest
|
|
559
920
|
.spyOn(console, "error")
|
|
@@ -564,7 +925,6 @@ describe("handleWorkerMessage", () => {
|
|
|
564
925
|
payload: { groupId: "peer:g1", envelope: {} },
|
|
565
926
|
});
|
|
566
927
|
|
|
567
|
-
// Wait for the fire-and-forget catch block
|
|
568
928
|
await new Promise(process.nextTick);
|
|
569
929
|
|
|
570
930
|
expect(consoleError).toHaveBeenCalledWith(
|
|
@@ -618,7 +978,7 @@ describe("handleWorkerMessage", () => {
|
|
|
618
978
|
);
|
|
619
979
|
});
|
|
620
980
|
|
|
621
|
-
it("handles mcp-reauth-required", async () => {
|
|
981
|
+
it("handles mcp-reauth-required success, retry action, and popup success/failure", async () => {
|
|
622
982
|
mockGetRemoteMcpConnection.mockResolvedValue({
|
|
623
983
|
autoReconnectOAuth: true,
|
|
624
984
|
label: "conn",
|
|
@@ -631,10 +991,49 @@ describe("handleWorkerMessage", () => {
|
|
|
631
991
|
});
|
|
632
992
|
|
|
633
993
|
expect(mockReconnectMcpOAuth).toHaveBeenCalled();
|
|
634
|
-
expect(mockOrchestrator.agentWorker
|
|
994
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
635
995
|
type: "mcp-reauth-result",
|
|
636
996
|
payload: { connectionId: "c1", success: true },
|
|
637
997
|
});
|
|
998
|
+
|
|
999
|
+
// Reauth silent attempt fails -> triggers popup action
|
|
1000
|
+
mockGetRemoteMcpConnection.mockResolvedValue({
|
|
1001
|
+
autoReconnectOAuth: true,
|
|
1002
|
+
});
|
|
1003
|
+
mockReconnectMcpOAuth.mockResolvedValueOnce({
|
|
1004
|
+
success: false,
|
|
1005
|
+
error: "Token expired",
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await send({
|
|
1009
|
+
type: "mcp-reauth-required",
|
|
1010
|
+
payload: { connectionId: "c2" },
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const failedToastCall = mockShowToast.mock.calls[
|
|
1014
|
+
mockShowToast.mock.calls.length - 1
|
|
1015
|
+
] as [string, { action?: { onClick?: () => Promise<void> } }];
|
|
1016
|
+
const onClick = failedToastCall[1]?.action?.onClick;
|
|
1017
|
+
expect(typeof onClick).toBe("function");
|
|
1018
|
+
|
|
1019
|
+
// Click popup reconnect -> success
|
|
1020
|
+
mockReconnectMcpOAuth.mockResolvedValueOnce({ success: true });
|
|
1021
|
+
await onClick!();
|
|
1022
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
1023
|
+
'🔑 OAuth reconnected for "c2"',
|
|
1024
|
+
expect.objectContaining({ type: "success" }),
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
// Click popup reconnect -> failure
|
|
1028
|
+
mockReconnectMcpOAuth.mockResolvedValueOnce({
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: "User cancelled",
|
|
1031
|
+
});
|
|
1032
|
+
await onClick!();
|
|
1033
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
1034
|
+
'🔑 OAuth reconnect failed for "c2": User cancelled',
|
|
1035
|
+
expect.objectContaining({ type: "error" }),
|
|
1036
|
+
);
|
|
638
1037
|
});
|
|
639
1038
|
|
|
640
1039
|
it("handles mcp-reauth-required without auto-reconnect", async () => {
|
|
@@ -645,7 +1044,7 @@ describe("handleWorkerMessage", () => {
|
|
|
645
1044
|
payload: { connectionId: "c1" },
|
|
646
1045
|
});
|
|
647
1046
|
|
|
648
|
-
expect(mockOrchestrator.agentWorker
|
|
1047
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
649
1048
|
type: "mcp-reauth-result",
|
|
650
1049
|
payload: { connectionId: "c1", success: false },
|
|
651
1050
|
});
|
|
@@ -658,7 +1057,7 @@ describe("handleWorkerMessage", () => {
|
|
|
658
1057
|
|
|
659
1058
|
const localResolve = jest.fn();
|
|
660
1059
|
const localReject = jest.fn();
|
|
661
|
-
|
|
1060
|
+
globalNativeAi.pendingNativeAiResolvers = {
|
|
662
1061
|
task_123: { resolve: localResolve, reject: localReject },
|
|
663
1062
|
};
|
|
664
1063
|
|
|
@@ -676,7 +1075,6 @@ describe("handleWorkerMessage", () => {
|
|
|
676
1075
|
},
|
|
677
1076
|
});
|
|
678
1077
|
|
|
679
|
-
// Wait for the async IIFE inside handleWorkerMessage
|
|
680
1078
|
await new Promise(process.nextTick);
|
|
681
1079
|
|
|
682
1080
|
expect(mockTranslateText).toHaveBeenCalledWith(
|
|
@@ -688,30 +1086,69 @@ describe("handleWorkerMessage", () => {
|
|
|
688
1086
|
);
|
|
689
1087
|
expect(localResolve).toHaveBeenCalledWith("tres");
|
|
690
1088
|
expect(
|
|
691
|
-
|
|
1089
|
+
globalNativeAi.pendingNativeAiResolvers?.["task_123"],
|
|
692
1090
|
).toBeUndefined();
|
|
693
|
-
expect(mockOrchestrator.agentWorker
|
|
1091
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
694
1092
|
type: "native-ai-task-response",
|
|
695
1093
|
payload: { id: "task_123", response: "tres" },
|
|
696
1094
|
});
|
|
697
1095
|
});
|
|
698
1096
|
|
|
1097
|
+
it("handles onProgress callback and task execution without groupId", async () => {
|
|
1098
|
+
mockGetConfig.mockResolvedValue("local");
|
|
1099
|
+
mockSummarizeText.mockImplementation(
|
|
1100
|
+
async (_text: unknown, options: unknown) => {
|
|
1101
|
+
const opts = options as { onProgress?: (p: unknown) => void };
|
|
1102
|
+
opts.onProgress?.({
|
|
1103
|
+
status: "downloading",
|
|
1104
|
+
progress: 0.5,
|
|
1105
|
+
message: "downloading model",
|
|
1106
|
+
});
|
|
1107
|
+
return "summary result";
|
|
1108
|
+
},
|
|
1109
|
+
);
|
|
1110
|
+
mockGetApiKeyForRequest.mockResolvedValueOnce(null);
|
|
1111
|
+
|
|
1112
|
+
await send({
|
|
1113
|
+
type: "request-native-ai-task",
|
|
1114
|
+
payload: {
|
|
1115
|
+
id: "task_no_group",
|
|
1116
|
+
taskType: "summarize",
|
|
1117
|
+
input: { text: "text without group" },
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
await new Promise(process.nextTick);
|
|
1122
|
+
|
|
1123
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
1124
|
+
type: "native-ai-task-response",
|
|
1125
|
+
payload: { id: "task_no_group", response: "summary result" },
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
699
1129
|
it("handles active_provider preference with Prompt API and resolves local resolvers", async () => {
|
|
700
1130
|
mockGetConfig.mockResolvedValue("active_provider");
|
|
701
1131
|
mockOrchestrator.provider = "prompt_api";
|
|
702
1132
|
|
|
703
|
-
const mockPrompt = jest.fn(
|
|
1133
|
+
const mockPrompt = jest.fn<(_p?: string) => Promise<string>>(
|
|
1134
|
+
async () => "tres",
|
|
1135
|
+
);
|
|
704
1136
|
const mockDestroy = jest.fn();
|
|
705
|
-
const mockCreate = jest.fn
|
|
1137
|
+
const mockCreate = jest.fn<
|
|
1138
|
+
() => Promise<{
|
|
1139
|
+
prompt: typeof mockPrompt;
|
|
1140
|
+
destroy: typeof mockDestroy;
|
|
1141
|
+
}>
|
|
1142
|
+
>(async () => ({
|
|
706
1143
|
prompt: mockPrompt,
|
|
707
1144
|
destroy: mockDestroy,
|
|
708
1145
|
}));
|
|
709
1146
|
|
|
710
|
-
|
|
1147
|
+
globalNativeAi.LanguageModel = { create: mockCreate };
|
|
711
1148
|
|
|
712
1149
|
const localResolve = jest.fn();
|
|
713
1150
|
const localReject = jest.fn();
|
|
714
|
-
|
|
1151
|
+
globalNativeAi.pendingNativeAiResolvers = {
|
|
715
1152
|
task_456: { resolve: localResolve, reject: localReject },
|
|
716
1153
|
};
|
|
717
1154
|
|
|
@@ -737,15 +1174,15 @@ describe("handleWorkerMessage", () => {
|
|
|
737
1174
|
);
|
|
738
1175
|
expect(mockDestroy).toHaveBeenCalled();
|
|
739
1176
|
expect(localResolve).toHaveBeenCalledWith("tres");
|
|
740
|
-
expect(mockOrchestrator.agentWorker
|
|
1177
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
741
1178
|
type: "native-ai-task-response",
|
|
742
1179
|
payload: { id: "task_456", response: "tres" },
|
|
743
1180
|
});
|
|
744
1181
|
|
|
745
|
-
delete
|
|
1182
|
+
delete globalNativeAi.LanguageModel;
|
|
746
1183
|
});
|
|
747
1184
|
|
|
748
|
-
it("rejects local resolvers and sends error message on failure", async () => {
|
|
1185
|
+
it("rejects local resolvers and sends error message on failure with Error and non-Error", async () => {
|
|
749
1186
|
mockGetConfig.mockResolvedValue("local");
|
|
750
1187
|
mockSummarizeText.mockRejectedValue(
|
|
751
1188
|
new Error("Summarizer out of memory"),
|
|
@@ -753,7 +1190,7 @@ describe("handleWorkerMessage", () => {
|
|
|
753
1190
|
|
|
754
1191
|
const localResolve = jest.fn();
|
|
755
1192
|
const localReject = jest.fn();
|
|
756
|
-
|
|
1193
|
+
globalNativeAi.pendingNativeAiResolvers = {
|
|
757
1194
|
task_err: { resolve: localResolve, reject: localReject },
|
|
758
1195
|
};
|
|
759
1196
|
|
|
@@ -773,64 +1210,113 @@ describe("handleWorkerMessage", () => {
|
|
|
773
1210
|
expect.objectContaining({ message: "Summarizer out of memory" }),
|
|
774
1211
|
);
|
|
775
1212
|
expect(
|
|
776
|
-
|
|
1213
|
+
globalNativeAi.pendingNativeAiResolvers?.["task_err"],
|
|
777
1214
|
).toBeUndefined();
|
|
778
|
-
expect(mockOrchestrator.agentWorker
|
|
1215
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
779
1216
|
type: "native-ai-task-response",
|
|
780
1217
|
payload: { id: "task_err", error: "Summarizer out of memory" },
|
|
781
1218
|
});
|
|
1219
|
+
|
|
1220
|
+
// Non-Error rejection without groupId
|
|
1221
|
+
mockSummarizeText.mockRejectedValueOnce("raw string failure");
|
|
1222
|
+
const stringResolve = jest.fn();
|
|
1223
|
+
const stringReject = jest.fn();
|
|
1224
|
+
globalNativeAi.pendingNativeAiResolvers = {
|
|
1225
|
+
task_string_err: { resolve: stringResolve, reject: stringReject },
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
await send({
|
|
1229
|
+
type: "request-native-ai-task",
|
|
1230
|
+
payload: {
|
|
1231
|
+
id: "task_string_err",
|
|
1232
|
+
taskType: "summarize",
|
|
1233
|
+
input: { text: "Long text" },
|
|
1234
|
+
},
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
await new Promise(process.nextTick);
|
|
1238
|
+
|
|
1239
|
+
expect(stringReject).toHaveBeenCalledWith(
|
|
1240
|
+
expect.objectContaining({ message: "raw string failure" }),
|
|
1241
|
+
);
|
|
1242
|
+
expect(mockOrchestrator.agentWorker?.postMessage).toHaveBeenCalledWith({
|
|
1243
|
+
type: "native-ai-task-response",
|
|
1244
|
+
payload: { id: "task_string_err", error: "raw string failure" },
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Non-Error rejection WITH groupId
|
|
1248
|
+
mockSummarizeText.mockRejectedValueOnce("raw string failure with group");
|
|
1249
|
+
const stringResolveGroup = jest.fn();
|
|
1250
|
+
const stringRejectGroup = jest.fn();
|
|
1251
|
+
globalNativeAi.pendingNativeAiResolvers = {
|
|
1252
|
+
task_string_group: {
|
|
1253
|
+
resolve: stringResolveGroup,
|
|
1254
|
+
reject: stringRejectGroup,
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
await send({
|
|
1259
|
+
type: "request-native-ai-task",
|
|
1260
|
+
payload: {
|
|
1261
|
+
id: "task_string_group",
|
|
1262
|
+
groupId: "g1",
|
|
1263
|
+
taskType: "summarize",
|
|
1264
|
+
input: { text: "Long text" },
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
await new Promise(process.nextTick);
|
|
1269
|
+
|
|
1270
|
+
expect(mockOrchestrator.events.emit).toHaveBeenCalledWith(
|
|
1271
|
+
"model-download-progress",
|
|
1272
|
+
expect.objectContaining({
|
|
1273
|
+
groupId: "g1",
|
|
1274
|
+
status: "error",
|
|
1275
|
+
message: "raw string failure with group",
|
|
1276
|
+
}),
|
|
1277
|
+
);
|
|
782
1278
|
});
|
|
783
1279
|
});
|
|
784
1280
|
|
|
785
|
-
/**
|
|
786
|
-
* Regression test for the missing `model` field on inFlightEffectiveProviderByGroup.
|
|
787
|
-
*
|
|
788
|
-
* Before the fix, `executeActiveProviderTask` resolved the model via:
|
|
789
|
-
* inFlightInfo?.providerConfig?.defaultModel || o.model
|
|
790
|
-
*
|
|
791
|
-
* This meant the conversation-pinned model was ignored and the provider's
|
|
792
|
-
* config-level defaultModel was silently substituted. The fix adds a `model`
|
|
793
|
-
* field directly to the in-flight entry so the resolved model is always used.
|
|
794
|
-
*/
|
|
795
1281
|
describe("request-native-ai-task uses inFlightEffectiveProviderByGroup.model", () => {
|
|
796
1282
|
it("passes the in-flight resolved model to formatRequest, not providerConfig.defaultModel", async () => {
|
|
797
1283
|
const { getProvider } = await import("../../../config/config.js");
|
|
798
|
-
(getProvider as jest.
|
|
1284
|
+
(getProvider as unknown as jest.Mock).mockReturnValue({
|
|
799
1285
|
name: "Test Provider",
|
|
800
1286
|
baseUrl: "https://api.test.example/v1",
|
|
801
1287
|
format: "openai",
|
|
802
1288
|
requiresApiKey: true,
|
|
803
1289
|
supportsStreaming: false,
|
|
804
|
-
defaultModel: "provider-default-model",
|
|
1290
|
+
defaultModel: "provider-default-model",
|
|
805
1291
|
});
|
|
806
1292
|
|
|
807
1293
|
mockGetConfig.mockResolvedValue("active_provider");
|
|
808
1294
|
mockOrchestrator.provider = "test-provider";
|
|
809
|
-
mockOrchestrator.model = "orchestrator-fallback-model";
|
|
1295
|
+
mockOrchestrator.model = "orchestrator-fallback-model";
|
|
810
1296
|
|
|
811
|
-
// The resolved (pinned) model recorded when the invocation started
|
|
812
1297
|
const resolvedModel = "claude-sonnet-4-5-pinned";
|
|
813
1298
|
mockOrchestrator.inFlightEffectiveProviderByGroup.set("g1", {
|
|
814
1299
|
providerId: "test-provider",
|
|
815
1300
|
providerConfig: {
|
|
1301
|
+
...dummyProviderConfig,
|
|
816
1302
|
name: "Test Provider",
|
|
817
1303
|
baseUrl: "https://api.test.example/v1",
|
|
818
1304
|
format: "openai",
|
|
819
1305
|
requiresApiKey: true,
|
|
820
1306
|
defaultModel: "provider-default-model",
|
|
821
1307
|
},
|
|
822
|
-
// ← This field was missing before the fix; without it, formatRequest
|
|
823
|
-
// would have received 'provider-default-model' or 'orchestrator-fallback-model'.
|
|
824
1308
|
model: resolvedModel,
|
|
825
1309
|
});
|
|
826
1310
|
|
|
827
|
-
global.fetch =
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1311
|
+
global.fetch = jest
|
|
1312
|
+
.fn<(...args: unknown[]) => Promise<Response>>()
|
|
1313
|
+
.mockResolvedValue({
|
|
1314
|
+
ok: true,
|
|
1315
|
+
json: () =>
|
|
1316
|
+
Promise.resolve({
|
|
1317
|
+
choices: [{ message: { content: "Summarized." } }],
|
|
1318
|
+
}),
|
|
1319
|
+
} as unknown as Response);
|
|
834
1320
|
|
|
835
1321
|
mockParseResponse.mockReturnValue({
|
|
836
1322
|
content: [{ type: "text", text: "Summarized." }],
|
|
@@ -848,8 +1334,6 @@ describe("handleWorkerMessage", () => {
|
|
|
848
1334
|
|
|
849
1335
|
await new Promise(process.nextTick);
|
|
850
1336
|
|
|
851
|
-
// formatRequest must have been called with the resolved pinned model,
|
|
852
|
-
// not providerConfig.defaultModel or o.model.
|
|
853
1337
|
expect(mockFormatRequest).toHaveBeenCalledWith(
|
|
854
1338
|
expect.anything(),
|
|
855
1339
|
expect.anything(),
|
|
@@ -857,5 +1341,41 @@ describe("handleWorkerMessage", () => {
|
|
|
857
1341
|
expect.objectContaining({ model: resolvedModel }),
|
|
858
1342
|
);
|
|
859
1343
|
});
|
|
1344
|
+
|
|
1345
|
+
it("falls back to providerConfig.defaultModel when inFlightInfo.model is not set", async () => {
|
|
1346
|
+
mockGetConfig.mockResolvedValue("active_provider");
|
|
1347
|
+
mockOrchestrator.inFlightEffectiveProviderByGroup.set("g1", {
|
|
1348
|
+
providerId: "test-provider",
|
|
1349
|
+
providerConfig: {
|
|
1350
|
+
...dummyProviderConfig,
|
|
1351
|
+
defaultModel: "provider-config-default-model",
|
|
1352
|
+
},
|
|
1353
|
+
model: "",
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
await send({
|
|
1357
|
+
type: "request-native-ai-task",
|
|
1358
|
+
payload: {
|
|
1359
|
+
id: "model-fallback-task",
|
|
1360
|
+
groupId: "g1",
|
|
1361
|
+
taskType: "summarize",
|
|
1362
|
+
input: { text: "A very long article…" },
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
await new Promise(process.nextTick);
|
|
1367
|
+
|
|
1368
|
+
expect(mockFormatRequest).toHaveBeenCalledWith(
|
|
1369
|
+
expect.anything(),
|
|
1370
|
+
expect.anything(),
|
|
1371
|
+
expect.anything(),
|
|
1372
|
+
expect.objectContaining({ model: "provider-config-default-model" }),
|
|
1373
|
+
);
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
it("safely ignores unrecognized worker message types", async () => {
|
|
1378
|
+
await send({ type: "unknown-type-xyz", payload: {} });
|
|
1379
|
+
expect(mockOrchestrator.events.emit).not.toHaveBeenCalled();
|
|
860
1380
|
});
|
|
861
1381
|
});
|