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