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
|
@@ -78,6 +78,7 @@ import { ensureControlPlaneConnected } from "../utils/initControlPlane.js";
|
|
|
78
78
|
import { applyAllChannelRunningStates } from "./utils/operations/channel.js";
|
|
79
79
|
|
|
80
80
|
import type { ShadowClawDatabase } from "../../db/db.js";
|
|
81
|
+
import type { InboundMessage } from "../../subsystems/channels/types.js";
|
|
81
82
|
import type { A2UIAction } from "../../ui/a2ui/types.js";
|
|
82
83
|
import type { DirectToolCommandPolicy } from "./utils/types.js";
|
|
83
84
|
|
|
@@ -139,7 +140,7 @@ export class Orchestrator {
|
|
|
139
140
|
|
|
140
141
|
meshLlmHost: string = "";
|
|
141
142
|
|
|
142
|
-
messageQueue:
|
|
143
|
+
messageQueue: InboundMessage[] = [];
|
|
143
144
|
model: string = getDefaultProvider().defaultModel;
|
|
144
145
|
|
|
145
146
|
/** Peer groupIds where the A2A task has reached a terminal state */
|
|
@@ -503,15 +504,7 @@ export class Orchestrator {
|
|
|
503
504
|
if (!storedKey) {
|
|
504
505
|
this.#encryptedApiKey = "";
|
|
505
506
|
} else {
|
|
506
|
-
|
|
507
|
-
// We now store the encrypted key directly in the field.
|
|
508
|
-
// decryptValue is only called on-demand.
|
|
509
|
-
this.#encryptedApiKey = storedKey;
|
|
510
|
-
} catch (e) {
|
|
511
|
-
console.warn("[Orchestrator] Failed to load API key:", e);
|
|
512
|
-
|
|
513
|
-
this.#encryptedApiKey = "";
|
|
514
|
-
}
|
|
507
|
+
this.#encryptedApiKey = storedKey;
|
|
515
508
|
}
|
|
516
509
|
|
|
517
510
|
this.#apiKeyCache = null; // Invalidate cache
|
|
@@ -1,194 +1,496 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
1
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
2
2
|
|
|
3
|
+
import { CONFIG_KEYS, DEFAULT_GROUP_ID } from "../../../config/config.js";
|
|
4
|
+
import type { ProviderConfig } from "../../../config/config.js";
|
|
5
|
+
import type { ConversationMessage } from "../../../content/types.js";
|
|
6
|
+
import type { ShadowClawDatabase } from "../../../db/db.js";
|
|
7
|
+
import type { GroupMeta } from "../../../db/types.js";
|
|
3
8
|
import type { Orchestrator } from "../orchestrator.js";
|
|
4
9
|
|
|
10
|
+
const mockReadGroupFile =
|
|
11
|
+
jest.fn<
|
|
12
|
+
(
|
|
13
|
+
db: ShadowClawDatabase,
|
|
14
|
+
groupId: string,
|
|
15
|
+
filename: string,
|
|
16
|
+
) => Promise<string>
|
|
17
|
+
>();
|
|
5
18
|
jest.unstable_mockModule("../../../storage/readGroupFile.js", () => ({
|
|
6
|
-
readGroupFile:
|
|
19
|
+
readGroupFile: mockReadGroupFile,
|
|
7
20
|
}));
|
|
8
21
|
|
|
22
|
+
const mockBuildConversationMessages =
|
|
23
|
+
jest.fn<(groupId: string, limit: number) => Promise<ConversationMessage[]>>();
|
|
9
24
|
jest.unstable_mockModule("../../../db/buildConversationMessages.js", () => ({
|
|
10
|
-
buildConversationMessages:
|
|
25
|
+
buildConversationMessages: mockBuildConversationMessages,
|
|
11
26
|
}));
|
|
12
27
|
|
|
28
|
+
const mockGetConfig =
|
|
29
|
+
jest.fn<(db: ShadowClawDatabase, key: string) => Promise<string | null>>();
|
|
13
30
|
jest.unstable_mockModule("../../../db/getConfig.js", () => ({
|
|
14
|
-
getConfig:
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
jest.unstable_mockModule("../../../db/saveMessage.js", () => ({
|
|
18
|
-
saveMessage: jest.fn<any>().mockResolvedValue(1),
|
|
31
|
+
getConfig: mockGetConfig,
|
|
19
32
|
}));
|
|
20
33
|
|
|
34
|
+
let mockGroupsList: GroupMeta[] = [];
|
|
21
35
|
jest.unstable_mockModule("../../../db/groups.js", () => ({
|
|
22
|
-
getGroupMetadata: jest.fn<
|
|
23
|
-
saveGroupMetadata: jest
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
getGroupMetadata: jest.fn<() => Promise<GroupMeta[]>>().mockResolvedValue([]),
|
|
37
|
+
saveGroupMetadata: jest
|
|
38
|
+
.fn<() => Promise<void>>()
|
|
39
|
+
.mockResolvedValue(undefined),
|
|
40
|
+
createGroup: jest
|
|
41
|
+
.fn<() => Promise<GroupMeta>>()
|
|
42
|
+
.mockResolvedValue({} as GroupMeta),
|
|
43
|
+
renameGroup: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
44
|
+
updateGroupToolTags: jest
|
|
45
|
+
.fn<() => Promise<void>>()
|
|
46
|
+
.mockResolvedValue(undefined),
|
|
47
|
+
deleteGroupMetadata: jest
|
|
48
|
+
.fn<() => Promise<void>>()
|
|
49
|
+
.mockResolvedValue(undefined),
|
|
50
|
+
listGroups: jest.fn<() => Promise<GroupMeta[]>>(async () => mockGroupsList),
|
|
51
|
+
reorderGroups: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
52
|
+
cloneGroup: jest
|
|
53
|
+
.fn<() => Promise<GroupMeta>>()
|
|
54
|
+
.mockResolvedValue({} as GroupMeta),
|
|
55
|
+
updateGroupPinnedProvider: jest
|
|
56
|
+
.fn<() => Promise<void>>()
|
|
57
|
+
.mockResolvedValue(undefined),
|
|
58
|
+
updateGroupSubagentSettings: jest
|
|
59
|
+
.fn<() => Promise<void>>()
|
|
60
|
+
.mockResolvedValue(undefined),
|
|
39
61
|
updateGroupProviderRuntimeOverrides: jest
|
|
40
|
-
.fn<
|
|
62
|
+
.fn<() => Promise<void>>()
|
|
41
63
|
.mockResolvedValue(undefined),
|
|
42
64
|
}));
|
|
43
65
|
|
|
66
|
+
const mockEnsureBuiltinAiPolyfills = jest
|
|
67
|
+
.fn<() => Promise<void>>()
|
|
68
|
+
.mockResolvedValue(undefined);
|
|
69
|
+
const mockSummarizeText = jest
|
|
70
|
+
.fn<(text: string, options: unknown) => Promise<string>>()
|
|
71
|
+
.mockResolvedValue("Mock task summary");
|
|
44
72
|
jest.unstable_mockModule(
|
|
45
73
|
"../../../subsystems/providers/builtin-ai-tasks.js",
|
|
46
74
|
() => ({
|
|
47
|
-
ensureBuiltinAiPolyfills:
|
|
48
|
-
summarizeText:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
createTaskInstanceWithFallback: jest
|
|
60
|
-
|
|
75
|
+
ensureBuiltinAiPolyfills: mockEnsureBuiltinAiPolyfills,
|
|
76
|
+
summarizeText: mockSummarizeText,
|
|
77
|
+
writeText: jest.fn<() => Promise<string>>().mockResolvedValue(""),
|
|
78
|
+
rewriteText: jest.fn<() => Promise<string>>().mockResolvedValue(""),
|
|
79
|
+
detectLanguage: jest.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
|
|
80
|
+
translateText: jest.fn<() => Promise<string>>().mockResolvedValue(""),
|
|
81
|
+
proofreadText: jest.fn<() => Promise<string>>().mockResolvedValue(""),
|
|
82
|
+
embedText: jest.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
|
|
83
|
+
isBuiltinTaskSupported: jest.fn<() => boolean>(() => true),
|
|
84
|
+
getPromptApiFallbackModel: jest
|
|
85
|
+
.fn<() => Promise<string>>()
|
|
86
|
+
.mockResolvedValue(""),
|
|
87
|
+
createTaskInstanceWithFallback: jest
|
|
88
|
+
.fn<() => Promise<unknown>>()
|
|
89
|
+
.mockResolvedValue({}),
|
|
90
|
+
isWebGpuAdapterAvailable: jest
|
|
91
|
+
.fn<() => Promise<boolean>>()
|
|
92
|
+
.mockResolvedValue(false),
|
|
61
93
|
}),
|
|
62
94
|
);
|
|
63
95
|
|
|
96
|
+
const mockIsPromptApiSupported = jest.fn<() => boolean>().mockReturnValue(true);
|
|
97
|
+
const mockCompactWithPromptApi = jest
|
|
98
|
+
.fn<
|
|
99
|
+
(
|
|
100
|
+
systemPrompt: string,
|
|
101
|
+
messages: unknown[],
|
|
102
|
+
signal: AbortSignal,
|
|
103
|
+
onMessage: (msg: unknown) => Promise<void>,
|
|
104
|
+
groupId: string,
|
|
105
|
+
) => Promise<string>
|
|
106
|
+
>()
|
|
107
|
+
.mockResolvedValue("Mock prompt API summary");
|
|
108
|
+
|
|
64
109
|
jest.unstable_mockModule(
|
|
65
110
|
"../../../subsystems/providers/prompt-api-provider.js",
|
|
66
111
|
() => ({
|
|
67
|
-
isPromptApiSupported:
|
|
68
|
-
compactWithPromptApi:
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
112
|
+
isPromptApiSupported: mockIsPromptApiSupported,
|
|
113
|
+
compactWithPromptApi: mockCompactWithPromptApi,
|
|
114
|
+
invokeWithPromptApi: jest
|
|
115
|
+
.fn<() => Promise<void>>()
|
|
116
|
+
.mockResolvedValue(undefined),
|
|
72
117
|
}),
|
|
73
118
|
);
|
|
74
119
|
|
|
120
|
+
const mockDeliverResponse = jest
|
|
121
|
+
.fn<
|
|
122
|
+
(
|
|
123
|
+
o: Orchestrator,
|
|
124
|
+
db: ShadowClawDatabase,
|
|
125
|
+
groupId: string,
|
|
126
|
+
text: string,
|
|
127
|
+
) => Promise<void>
|
|
128
|
+
>()
|
|
129
|
+
.mockResolvedValue(undefined);
|
|
130
|
+
|
|
131
|
+
jest.unstable_mockModule("./deliverResponse.js", () => ({
|
|
132
|
+
deliverResponse: mockDeliverResponse,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
const mockHandleWorkerMessage = jest
|
|
136
|
+
.fn<
|
|
137
|
+
(o: Orchestrator, db: ShadowClawDatabase, msg: unknown) => Promise<void>
|
|
138
|
+
>()
|
|
139
|
+
.mockResolvedValue(undefined);
|
|
140
|
+
|
|
141
|
+
jest.unstable_mockModule("./handleWorkerMessage.js", () => ({
|
|
142
|
+
handleWorkerMessage: mockHandleWorkerMessage,
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
let mockPeerState: unknown = null;
|
|
146
|
+
jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
|
|
147
|
+
orchestratorStore: {
|
|
148
|
+
getPeerState: jest.fn(() => mockPeerState),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
const { compactContext } = await import("./compactContext.js");
|
|
153
|
+
const { Orchestrator: Orch } = await import("../orchestrator.js");
|
|
154
|
+
|
|
75
155
|
describe("compactContext", () => {
|
|
76
|
-
let db:
|
|
156
|
+
let db: ShadowClawDatabase;
|
|
77
157
|
let o: Orchestrator;
|
|
78
|
-
let compactContext: any;
|
|
79
158
|
|
|
80
|
-
beforeEach(
|
|
81
|
-
|
|
82
|
-
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
jest.clearAllMocks();
|
|
161
|
+
db = {} as unknown as ShadowClawDatabase;
|
|
162
|
+
mockPeerState = null;
|
|
163
|
+
mockGroupsList = [
|
|
164
|
+
{
|
|
165
|
+
groupId: "group-pinned",
|
|
166
|
+
name: "Pinned Group",
|
|
167
|
+
createdAt: Date.now(),
|
|
168
|
+
pinnedProvider: "transformers_js_local",
|
|
169
|
+
pinnedModel: "onnx-community/Llama-3.2-1B-Instruct",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
groupId: "group-pinned-no-model",
|
|
173
|
+
name: "Pinned Group No Model",
|
|
174
|
+
createdAt: Date.now(),
|
|
175
|
+
pinnedProvider: "openrouter",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
groupId: "group-pinned-unknown-prov",
|
|
179
|
+
name: "Pinned Group Unknown Prov",
|
|
180
|
+
createdAt: Date.now(),
|
|
181
|
+
pinnedProvider: "unknown_prov_xyz",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
mockReadGroupFile.mockRejectedValue(new Error("File not found"));
|
|
186
|
+
mockBuildConversationMessages.mockResolvedValue([]);
|
|
187
|
+
mockGetConfig.mockResolvedValue(null);
|
|
188
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
189
|
+
|
|
83
190
|
o = new Orch();
|
|
191
|
+
jest.spyOn(o, "getApiKey").mockResolvedValue("valid-api-key");
|
|
84
192
|
o.provider = "openrouter";
|
|
85
|
-
o.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
193
|
+
o.model = "openai/gpt-4o";
|
|
194
|
+
o.providerConfig = {
|
|
195
|
+
id: "openrouter",
|
|
196
|
+
name: "OpenRouter",
|
|
197
|
+
defaultModel: "openai/gpt-4o",
|
|
198
|
+
requiresApiKey: false,
|
|
199
|
+
} as ProviderConfig;
|
|
200
|
+
o.agentWorker = { postMessage: jest.fn() } as unknown as Worker;
|
|
201
|
+
o.handleCompactDone = jest
|
|
202
|
+
.fn<() => Promise<void>>()
|
|
203
|
+
.mockResolvedValue(undefined);
|
|
89
204
|
});
|
|
90
205
|
|
|
91
|
-
it("
|
|
206
|
+
it("should emit error and provider-help if provider requires API key and none is set", async () => {
|
|
92
207
|
o.provider = "openrouter";
|
|
93
|
-
o.providerConfig = {
|
|
208
|
+
o.providerConfig = {
|
|
209
|
+
id: "openrouter",
|
|
210
|
+
name: "OpenRouter",
|
|
211
|
+
defaultModel: "openai/gpt-4o",
|
|
212
|
+
requiresApiKey: true,
|
|
213
|
+
} as ProviderConfig;
|
|
94
214
|
jest.spyOn(o, "getApiKey").mockResolvedValue("");
|
|
95
215
|
|
|
96
|
-
const
|
|
97
|
-
|
|
216
|
+
const helpEvents: unknown[] = [];
|
|
217
|
+
const errorEvents: unknown[] = [];
|
|
218
|
+
o.events.on("provider-help", (e) => helpEvents.push(e));
|
|
219
|
+
o.events.on("error", (e) => errorEvents.push(e));
|
|
98
220
|
|
|
99
221
|
await compactContext(o, db, "group-1");
|
|
100
222
|
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
223
|
+
expect(helpEvents).toHaveLength(1);
|
|
224
|
+
expect(errorEvents).toHaveLength(1);
|
|
225
|
+
expect(errorEvents[0]).toEqual({
|
|
226
|
+
groupId: "group-1",
|
|
227
|
+
error: "API key not configured. Cannot compact context.",
|
|
228
|
+
});
|
|
103
229
|
});
|
|
104
230
|
|
|
105
|
-
it("
|
|
106
|
-
jest
|
|
231
|
+
it("should fetch specific API key when effective provider differs from orchestrator provider", async () => {
|
|
232
|
+
const specificApiKeySpy = jest
|
|
233
|
+
.spyOn(o, "getApiKeyForSpecificProvider")
|
|
234
|
+
.mockResolvedValueOnce("specific-key");
|
|
235
|
+
mockGroupsList.push({
|
|
236
|
+
groupId: "group-different-prov",
|
|
237
|
+
name: "Diff Prov",
|
|
238
|
+
createdAt: Date.now(),
|
|
239
|
+
pinnedProvider: "anthropic",
|
|
240
|
+
pinnedModel: "claude-3-5-sonnet",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await compactContext(o, db, "group-different-prov");
|
|
244
|
+
|
|
245
|
+
expect(specificApiKeySpy).toHaveBeenCalledWith(db, "anthropic");
|
|
246
|
+
expect(o.agentWorker?.postMessage).toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should emit error and abort if orchestrator state is not idle", async () => {
|
|
107
250
|
o.setState("thinking");
|
|
108
251
|
|
|
109
|
-
const
|
|
110
|
-
o.events.on("error", (e
|
|
252
|
+
const errorEvents: unknown[] = [];
|
|
253
|
+
o.events.on("error", (e) => errorEvents.push(e));
|
|
111
254
|
|
|
112
255
|
await compactContext(o, db, "group-1");
|
|
113
256
|
|
|
114
|
-
expect(
|
|
115
|
-
expect(
|
|
257
|
+
expect(errorEvents).toHaveLength(1);
|
|
258
|
+
expect(errorEvents[0]).toEqual({
|
|
259
|
+
groupId: "group-1",
|
|
260
|
+
error:
|
|
261
|
+
"Cannot compact while processing. Wait for the current response to finish.",
|
|
262
|
+
});
|
|
116
263
|
});
|
|
117
264
|
|
|
118
|
-
it("
|
|
119
|
-
|
|
120
|
-
const postMessage = jest.fn();
|
|
121
|
-
o.agentWorker = { postMessage } as any;
|
|
265
|
+
it("should read memory from MEMORY.md if available", async () => {
|
|
266
|
+
mockReadGroupFile.mockResolvedValueOnce("# Context Memory");
|
|
122
267
|
|
|
123
268
|
await compactContext(o, db, "group-1");
|
|
124
269
|
|
|
270
|
+
const postMessage = o.agentWorker?.postMessage as jest.Mock;
|
|
125
271
|
expect(postMessage).toHaveBeenCalled();
|
|
126
|
-
const payload
|
|
127
|
-
|
|
128
|
-
|
|
272
|
+
const payload = postMessage.mock.calls[0][0] as {
|
|
273
|
+
payload: { memory: string };
|
|
274
|
+
};
|
|
275
|
+
expect(payload.payload.memory).toBe("# Context Memory");
|
|
129
276
|
});
|
|
130
277
|
|
|
131
|
-
it("
|
|
132
|
-
|
|
133
|
-
const postMessage = jest.fn();
|
|
134
|
-
o.agentWorker = { postMessage } as any;
|
|
135
|
-
|
|
136
|
-
o.provider = "openrouter";
|
|
137
|
-
o.model = "gpt-4o";
|
|
278
|
+
it("should use peerState from orchestratorStore when available", async () => {
|
|
279
|
+
mockPeerState = { peerId: "peer-abc", typing: true };
|
|
138
280
|
|
|
139
|
-
await compactContext(o, db, "group-
|
|
281
|
+
await compactContext(o, db, "group-1");
|
|
140
282
|
|
|
141
|
-
expect(postMessage).toHaveBeenCalled();
|
|
142
|
-
const payload: any = postMessage.mock.calls[0][0];
|
|
143
|
-
expect(payload.type).toBe("compact");
|
|
144
|
-
expect(payload.payload.provider).toBe("transformers_js_local");
|
|
145
|
-
expect(payload.payload.model).toBe("onnx-community/Llama-3.2-1B-Instruct");
|
|
283
|
+
expect(o.agentWorker?.postMessage).toHaveBeenCalled();
|
|
146
284
|
});
|
|
147
285
|
|
|
148
|
-
it("
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
286
|
+
it("should resolve fallback model for prompt_api when browser-built-in and not supported", async () => {
|
|
287
|
+
o.provider = "prompt_api";
|
|
288
|
+
o.model = "browser-built-in";
|
|
289
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
290
|
+
mockGetConfig.mockImplementation(async (_db, key) => {
|
|
291
|
+
if (key === CONFIG_KEYS.PROMPT_API_FALLBACK_MODEL) {
|
|
292
|
+
return "custom-fallback-model";
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
});
|
|
159
296
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
(
|
|
163
|
-
|
|
164
|
-
{ role: "assistant", content: "Hello" },
|
|
165
|
-
]);
|
|
297
|
+
await compactContext(o, db, "group-1");
|
|
298
|
+
|
|
299
|
+
expect(mockEnsureBuiltinAiPolyfills).toHaveBeenCalled();
|
|
300
|
+
});
|
|
166
301
|
|
|
167
|
-
|
|
302
|
+
it("should resolve default fallback model when PROMPT_API_FALLBACK_MODEL is null", async () => {
|
|
303
|
+
o.provider = "prompt_api";
|
|
304
|
+
o.model = "";
|
|
305
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
306
|
+
mockGetConfig.mockResolvedValue(null);
|
|
168
307
|
|
|
169
308
|
await compactContext(o, db, "group-1");
|
|
170
309
|
|
|
171
|
-
expect(
|
|
172
|
-
db,
|
|
173
|
-
"group-1",
|
|
174
|
-
"Mock summary from Task API",
|
|
175
|
-
);
|
|
310
|
+
expect(mockEnsureBuiltinAiPolyfills).toHaveBeenCalled();
|
|
176
311
|
});
|
|
177
312
|
|
|
178
|
-
it("
|
|
179
|
-
|
|
180
|
-
|
|
313
|
+
it("should handle pinned provider without pinned model using defaultModel or fallback", async () => {
|
|
314
|
+
await compactContext(o, db, "group-pinned-no-model");
|
|
315
|
+
const postMessage = o.agentWorker?.postMessage as jest.Mock;
|
|
316
|
+
expect(postMessage).toHaveBeenCalled();
|
|
317
|
+
const payload = postMessage.mock.calls[0][0] as {
|
|
318
|
+
payload: { provider: string };
|
|
319
|
+
};
|
|
320
|
+
expect(payload.payload.provider).toBe("openrouter");
|
|
321
|
+
|
|
322
|
+
// Also test group with unknown pinned provider
|
|
323
|
+
o.setState("idle");
|
|
324
|
+
jest
|
|
325
|
+
.spyOn(o, "getApiKeyForSpecificProvider")
|
|
326
|
+
.mockResolvedValue("specific-key");
|
|
327
|
+
await compactContext(o, db, "group-pinned-unknown-prov");
|
|
328
|
+
expect(postMessage).toHaveBeenCalledTimes(2);
|
|
329
|
+
});
|
|
181
330
|
|
|
182
|
-
|
|
183
|
-
o
|
|
184
|
-
o.
|
|
331
|
+
it("should default groupId to DEFAULT_GROUP_ID when omitted", async () => {
|
|
332
|
+
await compactContext(o, db);
|
|
333
|
+
const postMessage = o.agentWorker?.postMessage as jest.Mock;
|
|
334
|
+
expect(postMessage).toHaveBeenCalled();
|
|
335
|
+
const payload = postMessage.mock.calls[0][0] as {
|
|
336
|
+
payload: { groupId: string };
|
|
337
|
+
};
|
|
338
|
+
expect(payload.payload.groupId).toBe(DEFAULT_GROUP_ID);
|
|
339
|
+
});
|
|
185
340
|
|
|
186
|
-
|
|
341
|
+
describe("compactionPref === builtin_task_api", () => {
|
|
342
|
+
it("should summarize messages and call handleCompactDone on success", async () => {
|
|
343
|
+
mockGetConfig.mockImplementation(async (_db, key) => {
|
|
344
|
+
if (key === CONFIG_KEYS.COMPACTION_ENGINE_PREFERENCE) {
|
|
345
|
+
return "builtin_task_api";
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
});
|
|
349
|
+
mockBuildConversationMessages.mockResolvedValueOnce([
|
|
350
|
+
{ role: "user", content: "Hello" },
|
|
351
|
+
{ role: "assistant", content: { text: "World" } as unknown as string },
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
await compactContext(o, db, "group-1");
|
|
355
|
+
|
|
356
|
+
expect(mockSummarizeText).toHaveBeenCalled();
|
|
357
|
+
expect(o.handleCompactDone).toHaveBeenCalledWith(
|
|
358
|
+
db,
|
|
359
|
+
"group-1",
|
|
360
|
+
"Mock task summary",
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should catch error in summarizeText and deliver fallback response", async () => {
|
|
365
|
+
mockGetConfig.mockImplementation(async (_db, key) => {
|
|
366
|
+
if (key === CONFIG_KEYS.COMPACTION_ENGINE_PREFERENCE) {
|
|
367
|
+
return "builtin_task_api";
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
});
|
|
371
|
+
mockSummarizeText.mockRejectedValueOnce(new Error("Summarizer failed"));
|
|
372
|
+
|
|
373
|
+
await compactContext(o, db, "group-1");
|
|
374
|
+
|
|
375
|
+
expect(mockDeliverResponse).toHaveBeenCalledWith(
|
|
376
|
+
o,
|
|
377
|
+
db,
|
|
378
|
+
"group-1",
|
|
379
|
+
"⚠️ Built-in Task API compaction failed, falling back to provider: Summarizer failed",
|
|
380
|
+
);
|
|
381
|
+
expect(o.agentWorker?.postMessage).toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should catch non-Error thrown in summarizeText and deliver fallback response", async () => {
|
|
385
|
+
mockGetConfig.mockImplementation(async (_db, key) => {
|
|
386
|
+
if (key === CONFIG_KEYS.COMPACTION_ENGINE_PREFERENCE) {
|
|
387
|
+
return "builtin_task_api";
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
});
|
|
391
|
+
mockSummarizeText.mockRejectedValueOnce("Non-error string failure");
|
|
392
|
+
|
|
393
|
+
await compactContext(o, db, "group-1");
|
|
394
|
+
|
|
395
|
+
expect(mockDeliverResponse).toHaveBeenCalledWith(
|
|
396
|
+
o,
|
|
397
|
+
db,
|
|
398
|
+
"group-1",
|
|
399
|
+
"⚠️ Built-in Task API compaction failed, falling back to provider: Non-error string failure",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
187
403
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
404
|
+
describe("effectiveProviderId === prompt_api", () => {
|
|
405
|
+
it("should abort if prompt_api remains unsupported after polyfill attempt", async () => {
|
|
406
|
+
o.provider = "prompt_api";
|
|
407
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
408
|
+
|
|
409
|
+
const errorEvents: unknown[] = [];
|
|
410
|
+
const typingEvents: unknown[] = [];
|
|
411
|
+
o.events.on("error", (e) => errorEvents.push(e));
|
|
412
|
+
o.events.on("typing", (e) => typingEvents.push(e));
|
|
413
|
+
|
|
414
|
+
await compactContext(o, db, "group-1");
|
|
415
|
+
|
|
416
|
+
expect(mockEnsureBuiltinAiPolyfills).toHaveBeenCalled();
|
|
417
|
+
expect(errorEvents).toHaveLength(1);
|
|
418
|
+
expect(typingEvents).toContainEqual({
|
|
419
|
+
groupId: "group-1",
|
|
420
|
+
typing: false,
|
|
421
|
+
});
|
|
422
|
+
expect(o.state).toBe("idle");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should execute compactWithPromptApi, trigger handleWorkerMessage callback, and call handleCompactDone", async () => {
|
|
426
|
+
o.provider = "prompt_api";
|
|
427
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
428
|
+
|
|
429
|
+
await compactContext(o, db, "group-1");
|
|
430
|
+
|
|
431
|
+
expect(mockCompactWithPromptApi).toHaveBeenCalled();
|
|
432
|
+
const onMsgCb = mockCompactWithPromptApi.mock.calls[0][3];
|
|
433
|
+
await onMsgCb({ type: "stream-chunk" });
|
|
434
|
+
expect(mockHandleWorkerMessage).toHaveBeenCalledWith(o, db, {
|
|
435
|
+
type: "stream-chunk",
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(o.handleCompactDone).toHaveBeenCalledWith(
|
|
439
|
+
db,
|
|
440
|
+
"group-1",
|
|
441
|
+
"Mock prompt API summary",
|
|
442
|
+
);
|
|
443
|
+
expect(o.promptControllers.has("group-1")).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("should return early when compactWithPromptApi throws AbortError", async () => {
|
|
447
|
+
o.provider = "prompt_api";
|
|
448
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
449
|
+
|
|
450
|
+
const abortError = new Error("Aborted");
|
|
451
|
+
abortError.name = "AbortError";
|
|
452
|
+
mockCompactWithPromptApi.mockRejectedValueOnce(abortError);
|
|
453
|
+
|
|
454
|
+
await compactContext(o, db, "group-1");
|
|
455
|
+
|
|
456
|
+
expect(mockDeliverResponse).not.toHaveBeenCalled();
|
|
457
|
+
expect(o.promptControllers.has("group-1")).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should deliver error response when compactWithPromptApi throws regular Error", async () => {
|
|
461
|
+
o.provider = "prompt_api";
|
|
462
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
463
|
+
|
|
464
|
+
mockCompactWithPromptApi.mockRejectedValueOnce(
|
|
465
|
+
new Error("Prompt API crash"),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
await compactContext(o, db, "group-1");
|
|
469
|
+
|
|
470
|
+
expect(mockDeliverResponse).toHaveBeenCalledWith(
|
|
471
|
+
o,
|
|
472
|
+
db,
|
|
473
|
+
"group-1",
|
|
474
|
+
"⚠️ Error: Compaction failed: Prompt API crash",
|
|
475
|
+
);
|
|
476
|
+
expect(o.promptControllers.has("group-1")).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should deliver error response when compactWithPromptApi throws non-Error", async () => {
|
|
480
|
+
o.provider = "prompt_api";
|
|
481
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
482
|
+
|
|
483
|
+
mockCompactWithPromptApi.mockRejectedValueOnce("String error thrown");
|
|
484
|
+
|
|
485
|
+
await compactContext(o, db, "group-1");
|
|
486
|
+
|
|
487
|
+
expect(mockDeliverResponse).toHaveBeenCalledWith(
|
|
488
|
+
o,
|
|
489
|
+
db,
|
|
490
|
+
"group-1",
|
|
491
|
+
"⚠️ Error: Compaction failed: String error thrown",
|
|
492
|
+
);
|
|
493
|
+
expect(o.promptControllers.has("group-1")).toBe(false);
|
|
494
|
+
});
|
|
193
495
|
});
|
|
194
496
|
});
|