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