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,6 +1,87 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
1
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { OrchestratorState } from "../../orchestrator-state.js";
|
|
4
|
+
import type { ShadowClawDatabase } from "../../../../db/db.js";
|
|
5
|
+
import type { EventBus } from "../EventBus.js";
|
|
6
|
+
import type { ProviderConfig } from "../../../../config/config.js";
|
|
7
|
+
|
|
8
|
+
const mockGetConfig = jest.fn<(...args: unknown[]) => Promise<unknown>>();
|
|
9
|
+
jest.unstable_mockModule("../../../../db/getConfig.js", () => ({
|
|
10
|
+
getConfig: mockGetConfig,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockDefaultSetConfig = jest
|
|
14
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
15
|
+
.mockResolvedValue(undefined);
|
|
16
|
+
jest.unstable_mockModule("../../../../db/setConfig.js", () => ({
|
|
17
|
+
setConfig: mockDefaultSetConfig,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const mockFetchModelInfo = jest.fn<(...args: unknown[]) => Promise<void>>();
|
|
21
|
+
const mockGetModelInfo = jest
|
|
22
|
+
.fn<(...args: unknown[]) => unknown>()
|
|
23
|
+
.mockReturnValue(undefined);
|
|
24
|
+
jest.unstable_mockModule(
|
|
25
|
+
"../../../../subsystems/providers/model-registry.js",
|
|
26
|
+
() => ({
|
|
27
|
+
modelRegistry: {
|
|
28
|
+
fetchModelInfo: mockFetchModelInfo,
|
|
29
|
+
getModelInfo: mockGetModelInfo,
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const mockIsPromptApiSupported = jest.fn<() => boolean>();
|
|
35
|
+
jest.unstable_mockModule(
|
|
36
|
+
"../../../../subsystems/providers/prompt-api-provider.js",
|
|
37
|
+
() => ({
|
|
38
|
+
isPromptApiSupported: mockIsPromptApiSupported,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const mockToolsStore = {
|
|
43
|
+
activeProfileId: "default" as string | null,
|
|
44
|
+
enabledToolNames: new Set<string>(["tool1"]),
|
|
45
|
+
findProfilesForProvider: jest
|
|
46
|
+
.fn<(...args: unknown[]) => unknown[]>()
|
|
47
|
+
.mockReturnValue([]),
|
|
48
|
+
activateProfile: jest
|
|
49
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
50
|
+
.mockResolvedValue(undefined),
|
|
51
|
+
};
|
|
52
|
+
jest.unstable_mockModule("../../../../stores/tools.js", () => ({
|
|
53
|
+
toolsStore: mockToolsStore,
|
|
54
|
+
}));
|
|
55
|
+
const { PROVIDERS } = await import("../../../../config/config.js");
|
|
56
|
+
|
|
57
|
+
function makeProviderConfig(
|
|
58
|
+
overrides: Partial<ProviderConfig> = {},
|
|
59
|
+
): ProviderConfig {
|
|
60
|
+
return {
|
|
61
|
+
id: "test-provider",
|
|
62
|
+
name: "Test Provider",
|
|
63
|
+
baseUrl: "http://api/chat/completions",
|
|
64
|
+
format: "openai",
|
|
65
|
+
requiresApiKey: false,
|
|
66
|
+
apiKeyHeader: "Authorization",
|
|
67
|
+
headers: {},
|
|
68
|
+
supportsStreaming: true,
|
|
69
|
+
defaultModel: "model-1",
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
PROVIDERS.test_empty_models = makeProviderConfig({
|
|
75
|
+
id: "test_empty_models",
|
|
76
|
+
name: "Test Empty Models",
|
|
77
|
+
baseUrl: "http://api/chat/completions",
|
|
78
|
+
requiresApiKey: true,
|
|
79
|
+
supportsStreaming: false,
|
|
80
|
+
defaultModel: "fallback-default-model",
|
|
81
|
+
models: [],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const {
|
|
4
85
|
getApiKeyForHeaders,
|
|
5
86
|
getApiKeyForRequest,
|
|
6
87
|
getLlamafileSettings,
|
|
@@ -19,23 +100,31 @@ import {
|
|
|
19
100
|
setModel,
|
|
20
101
|
setPeerjsMyAlias,
|
|
21
102
|
setPeerjsPeerAliases,
|
|
103
|
+
autoActivateProfile,
|
|
22
104
|
setProvider,
|
|
23
105
|
pollTransformersProgress,
|
|
24
106
|
startTransformersProgressPolling,
|
|
25
107
|
stopTransformersProgressPolling,
|
|
26
108
|
cancelLlamafileRequest,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
import type { OrchestratorState } from "../../orchestrator-state.js";
|
|
109
|
+
} = await import("./provider.js");
|
|
30
110
|
|
|
31
|
-
function makeState(
|
|
111
|
+
function makeState(
|
|
112
|
+
overrides: Partial<OrchestratorState> = {},
|
|
113
|
+
): OrchestratorState {
|
|
32
114
|
return {
|
|
115
|
+
assistantName: "Assistant",
|
|
116
|
+
triggerPattern: new RegExp(""),
|
|
33
117
|
reasoningEffort: "none",
|
|
34
118
|
provider: "openrouter",
|
|
35
|
-
|
|
119
|
+
model: "test-model",
|
|
120
|
+
maxTokens: 8192,
|
|
121
|
+
providerConfig: makeProviderConfig({
|
|
36
122
|
id: "openrouter",
|
|
123
|
+
name: "OpenRouter",
|
|
37
124
|
baseUrl: "http://api/chat/completions",
|
|
38
|
-
|
|
125
|
+
requiresApiKey: true,
|
|
126
|
+
defaultModel: "test-model",
|
|
127
|
+
}),
|
|
39
128
|
llamafileMode: "server",
|
|
40
129
|
llamafileHost: "127.0.0.1",
|
|
41
130
|
llamafilePort: 8080,
|
|
@@ -44,15 +133,29 @@ function makeState(overrides: Partial<OrchestratorState> = {}) {
|
|
|
44
133
|
bedrockAuthMode: "provider_chain",
|
|
45
134
|
bedrockProfileFallback: "default",
|
|
46
135
|
bedrockRegionFallback: "us-east-1",
|
|
47
|
-
|
|
136
|
+
peerjsMyAlias: "me",
|
|
137
|
+
peerjsPeerAliases: {},
|
|
138
|
+
transformersProgressPollers: new Map<string, number>(),
|
|
48
139
|
...overrides,
|
|
49
140
|
} as unknown as OrchestratorState;
|
|
50
141
|
}
|
|
51
142
|
|
|
52
143
|
describe("provider operations", () => {
|
|
144
|
+
const mockDb = {} as unknown as ShadowClawDatabase;
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
jest.clearAllMocks();
|
|
148
|
+
mockToolsStore.activeProfileId = "default";
|
|
149
|
+
mockToolsStore.enabledToolNames = new Set(["tool1"]);
|
|
150
|
+
mockToolsStore.findProfilesForProvider.mockReturnValue([]);
|
|
151
|
+
mockToolsStore.activateProfile.mockResolvedValue(undefined);
|
|
152
|
+
});
|
|
153
|
+
|
|
53
154
|
it("getApiKeyForHeaders and getApiKeyForRequest return correct values", async () => {
|
|
54
|
-
const orchestratorWithKey
|
|
55
|
-
getApiKey: jest
|
|
155
|
+
const orchestratorWithKey = {
|
|
156
|
+
getApiKey: jest
|
|
157
|
+
.fn<() => Promise<string | null>>()
|
|
158
|
+
.mockResolvedValue("sk-secret-123"),
|
|
56
159
|
};
|
|
57
160
|
expect(await getApiKeyForHeaders(orchestratorWithKey)).toBe(
|
|
58
161
|
"sk-secret-123",
|
|
@@ -61,8 +164,10 @@ describe("provider operations", () => {
|
|
|
61
164
|
"sk-secret-123",
|
|
62
165
|
);
|
|
63
166
|
|
|
64
|
-
const orchestratorNoKey
|
|
65
|
-
getApiKey: jest
|
|
167
|
+
const orchestratorNoKey = {
|
|
168
|
+
getApiKey: jest
|
|
169
|
+
.fn<() => Promise<string | null>>()
|
|
170
|
+
.mockResolvedValue(null),
|
|
66
171
|
};
|
|
67
172
|
expect(await getApiKeyForHeaders(orchestratorNoKey)).toBeUndefined();
|
|
68
173
|
expect(await getApiKeyForRequest(orchestratorNoKey)).toBe("");
|
|
@@ -88,21 +193,33 @@ describe("provider operations", () => {
|
|
|
88
193
|
});
|
|
89
194
|
});
|
|
90
195
|
|
|
91
|
-
it("getAvailableProviders returns non-empty list of providers", () => {
|
|
196
|
+
it("getAvailableProviders returns non-empty list of providers and handles empty models array", () => {
|
|
92
197
|
const providers = getAvailableProviders();
|
|
93
198
|
expect(providers.length).toBeGreaterThan(0);
|
|
94
199
|
expect(providers.some((p) => p.id === "openrouter")).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("getReasoningConfig returns effort or undefined", () => {
|
|
98
|
-
const state1 = makeState({ reasoningEffort: "high" });
|
|
99
|
-
expect(getReasoningConfig(state1)).toEqual({ effort: "high" });
|
|
100
200
|
|
|
101
|
-
const
|
|
102
|
-
|
|
201
|
+
const emptyModelsProvider = providers.find(
|
|
202
|
+
(p) => p.id === "test_empty_models",
|
|
203
|
+
);
|
|
204
|
+
expect(emptyModelsProvider).toBeDefined();
|
|
205
|
+
expect(emptyModelsProvider?.models).toEqual(["fallback-default-model"]);
|
|
206
|
+
});
|
|
103
207
|
|
|
104
|
-
|
|
105
|
-
expect(getReasoningConfig(
|
|
208
|
+
it("getReasoningConfig returns effort or undefined for various effort inputs", () => {
|
|
209
|
+
expect(getReasoningConfig(makeState({ reasoningEffort: "high" }))).toEqual({
|
|
210
|
+
effort: "high",
|
|
211
|
+
});
|
|
212
|
+
expect(
|
|
213
|
+
getReasoningConfig(makeState({ reasoningEffort: "none" })),
|
|
214
|
+
).toBeUndefined();
|
|
215
|
+
expect(
|
|
216
|
+
getReasoningConfig(makeState({ reasoningEffort: "" })),
|
|
217
|
+
).toBeUndefined();
|
|
218
|
+
expect(
|
|
219
|
+
getReasoningConfig({
|
|
220
|
+
reasoningEffort: 123 as unknown as string,
|
|
221
|
+
} as unknown as OrchestratorState),
|
|
222
|
+
).toBeUndefined();
|
|
106
223
|
});
|
|
107
224
|
|
|
108
225
|
it("getProviderRuntimeHeaders returns correct headers for llamafile and bedrock", () => {
|
|
@@ -128,6 +245,12 @@ describe("provider operations", () => {
|
|
|
128
245
|
expect(llamaOverrideHeaders["x-llamafile-port"]).toBe("9000");
|
|
129
246
|
expect(llamaOverrideHeaders["x-llamafile-offline"]).toBe("true");
|
|
130
247
|
|
|
248
|
+
// Llamafile with invalid mode fallback
|
|
249
|
+
const llamaInvalidMode = getProviderRuntimeHeaders(state, "llamafile", "", {
|
|
250
|
+
llamafile: { mode: "invalid" as unknown as "server" },
|
|
251
|
+
});
|
|
252
|
+
expect(llamaInvalidMode["x-llamafile-mode"]).toBe("server");
|
|
253
|
+
|
|
131
254
|
// Bedrock without overrides
|
|
132
255
|
const bedrockHeaders = getProviderRuntimeHeaders(state, "bedrock_proxy");
|
|
133
256
|
expect(bedrockHeaders["x-bedrock-region"]).toBe("us-east-1");
|
|
@@ -151,21 +274,64 @@ describe("provider operations", () => {
|
|
|
151
274
|
expect(bedrockOverrideHeaders["x-bedrock-profile"]).toBe("custom");
|
|
152
275
|
expect(bedrockOverrideHeaders["x-bedrock-auth-mode"]).toBe("sso");
|
|
153
276
|
|
|
277
|
+
// Bedrock without region or profile
|
|
278
|
+
const emptyBedrockState = makeState({
|
|
279
|
+
bedrockRegionFallback: "",
|
|
280
|
+
bedrockProfileFallback: "",
|
|
281
|
+
bedrockAuthMode: "sso",
|
|
282
|
+
});
|
|
283
|
+
const emptyBedrockHeaders = getProviderRuntimeHeaders(
|
|
284
|
+
emptyBedrockState,
|
|
285
|
+
"bedrock_proxy",
|
|
286
|
+
);
|
|
287
|
+
expect(emptyBedrockHeaders["x-bedrock-region"]).toBeUndefined();
|
|
288
|
+
expect(emptyBedrockHeaders["x-bedrock-profile"]).toBeUndefined();
|
|
289
|
+
expect(emptyBedrockHeaders["x-bedrock-auth-mode"]).toBe("sso");
|
|
290
|
+
|
|
154
291
|
// Other provider
|
|
155
292
|
expect(getProviderRuntimeHeaders(state, "openrouter")).toEqual({});
|
|
156
293
|
});
|
|
157
294
|
|
|
158
295
|
it("applyLlamafileHeaders and applyMeshLlmHeaders update providerConfig headers", () => {
|
|
159
296
|
const llamaState = makeState({
|
|
160
|
-
providerConfig: {
|
|
297
|
+
providerConfig: makeProviderConfig({
|
|
298
|
+
id: "llamafile",
|
|
299
|
+
name: "Llamafile",
|
|
300
|
+
baseUrl: "http://127.0.0.1:8080/v1",
|
|
301
|
+
defaultModel: "llamafile",
|
|
302
|
+
}),
|
|
303
|
+
llamafileOffline: true,
|
|
161
304
|
});
|
|
162
305
|
applyLlamafileHeaders(llamaState);
|
|
163
306
|
expect(llamaState.providerConfig?.headers?.["x-llamafile-mode"]).toBe(
|
|
164
307
|
"server",
|
|
165
308
|
);
|
|
309
|
+
expect(llamaState.providerConfig?.headers?.["x-llamafile-offline"]).toBe(
|
|
310
|
+
"true",
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const llamaNoHeadersState = makeState({
|
|
314
|
+
providerConfig: makeProviderConfig({
|
|
315
|
+
id: "llamafile",
|
|
316
|
+
name: "Llamafile",
|
|
317
|
+
baseUrl: "http://127.0.0.1:8080/v1",
|
|
318
|
+
defaultModel: "llamafile",
|
|
319
|
+
headers: undefined,
|
|
320
|
+
}),
|
|
321
|
+
llamafileOffline: false,
|
|
322
|
+
});
|
|
323
|
+
applyLlamafileHeaders(llamaNoHeadersState);
|
|
324
|
+
expect(
|
|
325
|
+
llamaNoHeadersState.providerConfig?.headers?.["x-llamafile-offline"],
|
|
326
|
+
).toBe("false");
|
|
166
327
|
|
|
167
328
|
const meshState = makeState({
|
|
168
|
-
providerConfig: {
|
|
329
|
+
providerConfig: makeProviderConfig({
|
|
330
|
+
id: "mesh-llm",
|
|
331
|
+
name: "MeshLLM",
|
|
332
|
+
baseUrl: "https://mesh.cloud/v1",
|
|
333
|
+
defaultModel: "mesh",
|
|
334
|
+
}),
|
|
169
335
|
meshLlmHost: "https://mesh.custom.io",
|
|
170
336
|
});
|
|
171
337
|
applyMeshLlmHeaders(meshState);
|
|
@@ -173,30 +339,53 @@ describe("provider operations", () => {
|
|
|
173
339
|
"https://mesh.custom.io",
|
|
174
340
|
);
|
|
175
341
|
|
|
176
|
-
|
|
342
|
+
const meshNoHeaders = makeState({
|
|
343
|
+
providerConfig: makeProviderConfig({
|
|
344
|
+
id: "mesh-llm",
|
|
345
|
+
name: "MeshLLM",
|
|
346
|
+
baseUrl: "https://mesh.cloud/v1",
|
|
347
|
+
defaultModel: "mesh",
|
|
348
|
+
headers: undefined,
|
|
349
|
+
}),
|
|
350
|
+
meshLlmHost: "https://mesh.noheaders.io",
|
|
351
|
+
});
|
|
352
|
+
applyMeshLlmHeaders(meshNoHeaders);
|
|
353
|
+
expect(meshNoHeaders.providerConfig?.headers?.["x-mesh-llm-host"]).toBe(
|
|
354
|
+
"https://mesh.noheaders.io",
|
|
355
|
+
);
|
|
356
|
+
|
|
177
357
|
const otherState = makeState({
|
|
178
|
-
providerConfig: {
|
|
358
|
+
providerConfig: makeProviderConfig({
|
|
359
|
+
id: "anthropic",
|
|
360
|
+
name: "Anthropic",
|
|
361
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
362
|
+
format: "anthropic",
|
|
363
|
+
requiresApiKey: true,
|
|
364
|
+
defaultModel: "claude",
|
|
365
|
+
}),
|
|
179
366
|
});
|
|
180
367
|
applyLlamafileHeaders(otherState);
|
|
181
368
|
applyMeshLlmHeaders(otherState);
|
|
182
369
|
});
|
|
183
370
|
|
|
184
|
-
it("getTransformersStatusUrl computes status URL correctly", () => {
|
|
371
|
+
it("getTransformersStatusUrl computes status URL correctly across fallbacks", () => {
|
|
185
372
|
const state1 = makeState({
|
|
186
|
-
providerConfig: {
|
|
373
|
+
providerConfig: makeProviderConfig({
|
|
187
374
|
id: "transformers_js_local",
|
|
375
|
+
name: "Local",
|
|
188
376
|
baseUrl: "http://api/chat/completions",
|
|
189
|
-
|
|
377
|
+
defaultModel: "model",
|
|
378
|
+
}),
|
|
190
379
|
});
|
|
191
380
|
expect(getTransformersStatusUrl(state1)).toBe("http://api/status");
|
|
192
381
|
|
|
193
382
|
const inFlightMap = new Map();
|
|
194
383
|
inFlightMap.set("group-1", {
|
|
195
384
|
providerId: "transformers_js_local",
|
|
196
|
-
providerConfig: {
|
|
385
|
+
providerConfig: makeProviderConfig({
|
|
197
386
|
id: "transformers_js_local",
|
|
198
387
|
baseUrl: "http://custom-host/chat/completions",
|
|
199
|
-
}
|
|
388
|
+
}),
|
|
200
389
|
});
|
|
201
390
|
const state2 = makeState({
|
|
202
391
|
inFlightEffectiveProviderByGroup: inFlightMap,
|
|
@@ -204,40 +393,132 @@ describe("provider operations", () => {
|
|
|
204
393
|
expect(getTransformersStatusUrl(state2, "group-1")).toBe(
|
|
205
394
|
"http://custom-host/status",
|
|
206
395
|
);
|
|
396
|
+
|
|
397
|
+
// In flight without /chat/completions falls through
|
|
398
|
+
inFlightMap.set("group-2", {
|
|
399
|
+
providerId: "transformers_js_local",
|
|
400
|
+
providerConfig: makeProviderConfig({
|
|
401
|
+
id: "transformers_js_local",
|
|
402
|
+
baseUrl: "http://custom-host/other",
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
expect(getTransformersStatusUrl(state2, "group-2")).toBe(
|
|
406
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(getTransformersStatusUrl(state2, "group-missing")).toBe(
|
|
410
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
inFlightMap.set("group-openrouter", {
|
|
414
|
+
providerId: "openrouter",
|
|
415
|
+
providerConfig: makeProviderConfig({
|
|
416
|
+
id: "openrouter",
|
|
417
|
+
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
expect(getTransformersStatusUrl(state2, "group-openrouter")).toBe(
|
|
421
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
inFlightMap.set("group-no-base", {
|
|
425
|
+
providerId: "transformers_js_local",
|
|
426
|
+
providerConfig: makeProviderConfig({
|
|
427
|
+
id: "transformers_js_local",
|
|
428
|
+
baseUrl: "",
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
expect(getTransformersStatusUrl(state2, "group-no-base")).toBe(
|
|
432
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// ProviderConfig with empty id
|
|
436
|
+
const stateEmptyId = makeState({
|
|
437
|
+
providerConfig: makeProviderConfig({
|
|
438
|
+
id: "",
|
|
439
|
+
baseUrl: "http://anonymous/chat/completions",
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
expect(getTransformersStatusUrl(stateEmptyId)).toBe(
|
|
443
|
+
"http://anonymous/status",
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// ProviderConfig with empty baseUrl
|
|
447
|
+
const stateEmptyBase = makeState({
|
|
448
|
+
providerConfig: makeProviderConfig({
|
|
449
|
+
id: "transformers_js_local",
|
|
450
|
+
baseUrl: "",
|
|
451
|
+
}),
|
|
452
|
+
});
|
|
453
|
+
expect(getTransformersStatusUrl(stateEmptyBase)).toBe(
|
|
454
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Fallback when providerConfig has no /chat/completions
|
|
458
|
+
const stateNoChat = makeState({
|
|
459
|
+
providerConfig: makeProviderConfig({
|
|
460
|
+
id: "other",
|
|
461
|
+
name: "Other",
|
|
462
|
+
baseUrl: "http://api/other",
|
|
463
|
+
defaultModel: "model",
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
const origLocalBaseUrl = PROVIDERS.transformers_js_local.baseUrl;
|
|
467
|
+
PROVIDERS.transformers_js_local.baseUrl = "http://localhost:8888/other";
|
|
468
|
+
expect(getTransformersStatusUrl(stateNoChat)).toBe(
|
|
469
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
470
|
+
);
|
|
471
|
+
PROVIDERS.transformers_js_local.baseUrl = "";
|
|
472
|
+
expect(getTransformersStatusUrl(stateNoChat)).toBe(
|
|
473
|
+
"http://localhost:8888/transformers-js-proxy/status",
|
|
474
|
+
);
|
|
475
|
+
PROVIDERS.transformers_js_local.baseUrl = origLocalBaseUrl;
|
|
207
476
|
});
|
|
208
477
|
|
|
209
478
|
it("async setters update state and call setConfig", async () => {
|
|
210
479
|
const state = makeState();
|
|
211
|
-
const mockSetConfig = jest
|
|
480
|
+
const mockSetConfig = jest
|
|
481
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
482
|
+
.mockResolvedValue(undefined);
|
|
212
483
|
|
|
213
|
-
await setAssistantName(
|
|
214
|
-
state,
|
|
215
|
-
{} as any,
|
|
216
|
-
"NewAssistant",
|
|
217
|
-
mockSetConfig as any,
|
|
218
|
-
);
|
|
484
|
+
await setAssistantName(state, mockDb, "NewAssistant", mockSetConfig);
|
|
219
485
|
expect(state.assistantName).toBe("NewAssistant");
|
|
220
486
|
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
221
|
-
|
|
487
|
+
mockDb,
|
|
222
488
|
"assistant_name",
|
|
223
489
|
"NewAssistant",
|
|
224
490
|
);
|
|
225
491
|
|
|
226
492
|
await setBedrockSettings(
|
|
227
493
|
state,
|
|
228
|
-
|
|
494
|
+
mockDb,
|
|
229
495
|
{ authMode: "sso", profile: "prof1", region: "us-west-2" },
|
|
230
|
-
mockSetConfig
|
|
496
|
+
mockSetConfig,
|
|
231
497
|
);
|
|
232
498
|
expect(state.bedrockRegionFallback).toBe("us-west-2");
|
|
233
499
|
expect(state.bedrockProfileFallback).toBe("prof1");
|
|
234
500
|
expect(state.bedrockAuthMode).toBe("sso");
|
|
235
501
|
|
|
502
|
+
// Bedrock with non-sso authMode and non-string region/profile
|
|
503
|
+
await setBedrockSettings(
|
|
504
|
+
state,
|
|
505
|
+
mockDb,
|
|
506
|
+
{
|
|
507
|
+
authMode: "other",
|
|
508
|
+
profile: null as unknown as string,
|
|
509
|
+
region: null as unknown as string,
|
|
510
|
+
},
|
|
511
|
+
mockSetConfig,
|
|
512
|
+
);
|
|
513
|
+
expect(state.bedrockAuthMode).toBe("provider_chain");
|
|
514
|
+
expect(state.bedrockRegionFallback).toBe("");
|
|
515
|
+
expect(state.bedrockProfileFallback).toBe("");
|
|
516
|
+
|
|
236
517
|
await setLlamafileSettings(
|
|
237
518
|
state,
|
|
238
|
-
|
|
519
|
+
mockDb,
|
|
239
520
|
{ host: "192.168.1.1", mode: "cli", offline: true, port: 9999 },
|
|
240
|
-
mockSetConfig
|
|
521
|
+
mockSetConfig,
|
|
241
522
|
);
|
|
242
523
|
expect(state.llamafileMode).toBe("cli");
|
|
243
524
|
expect(state.llamafileHost).toBe("192.168.1.1");
|
|
@@ -246,62 +527,260 @@ describe("provider operations", () => {
|
|
|
246
527
|
|
|
247
528
|
await setMeshLlmSettings(
|
|
248
529
|
state,
|
|
249
|
-
|
|
530
|
+
mockDb,
|
|
250
531
|
{ host: "https://mesh.new.host" },
|
|
251
|
-
mockSetConfig
|
|
532
|
+
mockSetConfig,
|
|
252
533
|
);
|
|
253
534
|
expect(state.meshLlmHost).toBe("https://mesh.new.host");
|
|
254
535
|
|
|
255
|
-
await setModel(state,
|
|
536
|
+
await setModel(state, mockDb, "openrouter/free", mockSetConfig);
|
|
256
537
|
expect(state.model).toBe("openrouter/free");
|
|
257
538
|
|
|
258
|
-
await setPeerjsMyAlias(state,
|
|
539
|
+
await setPeerjsMyAlias(state, mockDb, "my-alias", mockSetConfig);
|
|
259
540
|
expect(state.peerjsMyAlias).toBe("my-alias");
|
|
260
541
|
|
|
261
542
|
await setPeerjsPeerAliases(
|
|
262
543
|
state,
|
|
263
|
-
|
|
544
|
+
mockDb,
|
|
264
545
|
{ peer1: "Alias 1" },
|
|
265
|
-
mockSetConfig
|
|
546
|
+
mockSetConfig,
|
|
266
547
|
);
|
|
267
548
|
expect(state.peerjsPeerAliases).toEqual({ peer1: "Alias 1" });
|
|
268
549
|
});
|
|
269
550
|
|
|
551
|
+
it("setModel resolves fallback model when Prompt API is unsupported", async () => {
|
|
552
|
+
const state = makeState({
|
|
553
|
+
provider: "prompt_api",
|
|
554
|
+
model: "browser-built-in",
|
|
555
|
+
});
|
|
556
|
+
const mockSetConfig = jest
|
|
557
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
558
|
+
.mockResolvedValue(undefined);
|
|
559
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
560
|
+
|
|
561
|
+
// With configured fallback
|
|
562
|
+
mockGetConfig.mockResolvedValueOnce("custom-qwen-model");
|
|
563
|
+
await setModel(state, mockDb, "browser-built-in", mockSetConfig);
|
|
564
|
+
expect(state.model).toBe("browser-built-in");
|
|
565
|
+
|
|
566
|
+
// With no configured fallback
|
|
567
|
+
mockGetConfig.mockResolvedValueOnce("");
|
|
568
|
+
await setModel(state, mockDb, "", mockSetConfig);
|
|
569
|
+
expect(state.model).toBe("");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("autoActivateProfile activates exact, provider-only, or returns early", async () => {
|
|
573
|
+
const state = makeState({ provider: "test-provider", model: "test-model" });
|
|
574
|
+
|
|
575
|
+
// Returns early when no tools enabled and no active profile
|
|
576
|
+
mockToolsStore.activeProfileId = null;
|
|
577
|
+
mockToolsStore.enabledToolNames = new Set();
|
|
578
|
+
await autoActivateProfile(state, mockDb);
|
|
579
|
+
expect(mockToolsStore.findProfilesForProvider).not.toHaveBeenCalled();
|
|
580
|
+
|
|
581
|
+
// Returns early when candidates is empty
|
|
582
|
+
mockToolsStore.activeProfileId = "prof-1";
|
|
583
|
+
mockToolsStore.enabledToolNames = new Set(["tool-1"]);
|
|
584
|
+
mockToolsStore.findProfilesForProvider.mockReturnValue([]);
|
|
585
|
+
await autoActivateProfile(state, mockDb);
|
|
586
|
+
expect(mockToolsStore.activateProfile).not.toHaveBeenCalled();
|
|
587
|
+
|
|
588
|
+
// Activates exact match
|
|
589
|
+
mockToolsStore.findProfilesForProvider.mockReturnValue([
|
|
590
|
+
{ id: "exact-prof", providerId: "test-provider", model: "test-model" },
|
|
591
|
+
]);
|
|
592
|
+
await autoActivateProfile(state, mockDb);
|
|
593
|
+
expect(mockToolsStore.activateProfile).toHaveBeenCalledWith(
|
|
594
|
+
mockDb,
|
|
595
|
+
"exact-prof",
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Activates provider-only match when exact match is absent
|
|
599
|
+
mockToolsStore.activateProfile.mockClear();
|
|
600
|
+
mockToolsStore.findProfilesForProvider.mockReturnValue([
|
|
601
|
+
{ id: "prov-prof", providerId: "test-provider", model: undefined },
|
|
602
|
+
]);
|
|
603
|
+
await autoActivateProfile(state, mockDb);
|
|
604
|
+
expect(mockToolsStore.activateProfile).toHaveBeenCalledWith(
|
|
605
|
+
mockDb,
|
|
606
|
+
"prov-prof",
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
// No matching candidate
|
|
610
|
+
mockToolsStore.activateProfile.mockClear();
|
|
611
|
+
mockToolsStore.findProfilesForProvider.mockReturnValue([
|
|
612
|
+
{ id: "other-prof", providerId: "other-provider", model: "other-model" },
|
|
613
|
+
]);
|
|
614
|
+
await autoActivateProfile(state, mockDb);
|
|
615
|
+
expect(mockToolsStore.activateProfile).not.toHaveBeenCalled();
|
|
616
|
+
});
|
|
617
|
+
|
|
270
618
|
it("setProvider switches provider, loads key, and updates config", async () => {
|
|
271
619
|
const state = makeState();
|
|
272
|
-
const mockSetConfig = jest
|
|
273
|
-
|
|
274
|
-
|
|
620
|
+
const mockSetConfig = jest
|
|
621
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
622
|
+
.mockResolvedValue(undefined);
|
|
623
|
+
const mockLoadKey = jest
|
|
624
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
625
|
+
.mockResolvedValue(undefined);
|
|
626
|
+
const mockGetKey = jest
|
|
627
|
+
.fn<() => Promise<string | undefined>>()
|
|
628
|
+
.mockResolvedValue("key123");
|
|
629
|
+
|
|
630
|
+
// Throws on unknown provider
|
|
631
|
+
await expect(
|
|
632
|
+
setProvider(
|
|
633
|
+
state,
|
|
634
|
+
mockDb,
|
|
635
|
+
"non-existent-provider",
|
|
636
|
+
{
|
|
637
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
638
|
+
getApiKeyForHeaders: mockGetKey,
|
|
639
|
+
},
|
|
640
|
+
mockSetConfig,
|
|
641
|
+
),
|
|
642
|
+
).rejects.toThrow("Unknown provider: non-existent-provider");
|
|
275
643
|
|
|
644
|
+
// Switches to openrouter
|
|
276
645
|
await setProvider(
|
|
277
646
|
state,
|
|
278
|
-
|
|
647
|
+
mockDb,
|
|
279
648
|
"openrouter",
|
|
280
649
|
{
|
|
281
|
-
loadApiKeyForProvider: mockLoadKey
|
|
282
|
-
getApiKeyForHeaders: mockGetKey
|
|
650
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
651
|
+
getApiKeyForHeaders: mockGetKey,
|
|
283
652
|
},
|
|
284
|
-
mockSetConfig
|
|
653
|
+
mockSetConfig,
|
|
285
654
|
);
|
|
286
655
|
|
|
287
656
|
expect(state.provider).toBe("openrouter");
|
|
288
|
-
expect(mockLoadKey).toHaveBeenCalledWith(
|
|
289
|
-
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
657
|
+
expect(mockLoadKey).toHaveBeenCalledWith(mockDb, "openrouter");
|
|
658
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
659
|
+
mockDb,
|
|
660
|
+
"provider",
|
|
661
|
+
"openrouter",
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Switches to prompt_api when unsupported
|
|
665
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
666
|
+
mockGetConfig.mockResolvedValueOnce("custom-prompt-fallback");
|
|
667
|
+
await setProvider(
|
|
668
|
+
state,
|
|
669
|
+
mockDb,
|
|
670
|
+
"prompt_api",
|
|
671
|
+
{
|
|
672
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
673
|
+
getApiKeyForHeaders: mockGetKey,
|
|
674
|
+
},
|
|
675
|
+
mockSetConfig,
|
|
676
|
+
);
|
|
677
|
+
expect(state.provider).toBe("prompt_api");
|
|
678
|
+
|
|
679
|
+
// Switches to prompt_api when supported
|
|
680
|
+
mockIsPromptApiSupported.mockReturnValue(true);
|
|
681
|
+
await setProvider(
|
|
682
|
+
state,
|
|
683
|
+
mockDb,
|
|
684
|
+
"prompt_api",
|
|
685
|
+
{
|
|
686
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
687
|
+
getApiKeyForHeaders: mockGetKey,
|
|
688
|
+
},
|
|
689
|
+
mockSetConfig,
|
|
690
|
+
);
|
|
691
|
+
expect(state.provider).toBe("prompt_api");
|
|
692
|
+
|
|
693
|
+
// Switches to prompt_api when unsupported but model is not browser-built-in
|
|
694
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
695
|
+
PROVIDERS.prompt_api.defaultModel = "custom-prompt-model";
|
|
696
|
+
await setProvider(
|
|
697
|
+
state,
|
|
698
|
+
mockDb,
|
|
699
|
+
"prompt_api",
|
|
700
|
+
{
|
|
701
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
702
|
+
getApiKeyForHeaders: mockGetKey,
|
|
703
|
+
},
|
|
704
|
+
mockSetConfig,
|
|
705
|
+
);
|
|
706
|
+
PROVIDERS.prompt_api.defaultModel = "browser-built-in";
|
|
290
707
|
});
|
|
291
708
|
|
|
292
|
-
it("
|
|
709
|
+
it("async setters work with default setConfig implementation", async () => {
|
|
293
710
|
const state = makeState();
|
|
294
|
-
|
|
295
|
-
|
|
711
|
+
await setAssistantName(state, mockDb, "DefaultAssistant");
|
|
712
|
+
expect(state.assistantName).toBe("DefaultAssistant");
|
|
713
|
+
expect(mockDefaultSetConfig).toHaveBeenCalledWith(
|
|
714
|
+
mockDb,
|
|
715
|
+
"assistant_name",
|
|
716
|
+
"DefaultAssistant",
|
|
717
|
+
);
|
|
296
718
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
719
|
+
await setBedrockSettings(state, mockDb, {
|
|
720
|
+
authMode: "sso",
|
|
721
|
+
profile: "p",
|
|
722
|
+
region: "r",
|
|
723
|
+
});
|
|
724
|
+
expect(state.bedrockRegionFallback).toBe("r");
|
|
725
|
+
|
|
726
|
+
await setLlamafileSettings(state, mockDb, {
|
|
727
|
+
host: "1.2.3.4",
|
|
728
|
+
mode: "server",
|
|
729
|
+
offline: false,
|
|
730
|
+
port: 8080,
|
|
731
|
+
});
|
|
732
|
+
expect(state.llamafileHost).toBe("1.2.3.4");
|
|
733
|
+
|
|
734
|
+
await setMeshLlmSettings(state, mockDb, { host: "https://mesh.default" });
|
|
735
|
+
expect(state.meshLlmHost).toBe("https://mesh.default");
|
|
736
|
+
|
|
737
|
+
await setModel(state, mockDb, "openrouter/free");
|
|
738
|
+
expect(state.model).toBe("openrouter/free");
|
|
739
|
+
|
|
740
|
+
await setPeerjsMyAlias(state, mockDb, "alias-default");
|
|
741
|
+
expect(state.peerjsMyAlias).toBe("alias-default");
|
|
742
|
+
|
|
743
|
+
await setPeerjsPeerAliases(state, mockDb, { p: "alias" });
|
|
744
|
+
expect(state.peerjsPeerAliases).toEqual({ p: "alias" });
|
|
745
|
+
|
|
746
|
+
mockIsPromptApiSupported.mockReturnValue(false);
|
|
747
|
+
mockGetConfig.mockResolvedValueOnce("");
|
|
748
|
+
const mockLoadKey = jest
|
|
749
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
750
|
+
.mockResolvedValue(undefined);
|
|
751
|
+
const mockGetKey = jest
|
|
752
|
+
.fn<() => Promise<string | undefined>>()
|
|
753
|
+
.mockResolvedValue("");
|
|
754
|
+
await setProvider(state, mockDb, "prompt_api", {
|
|
755
|
+
loadApiKeyForProvider: mockLoadKey,
|
|
756
|
+
getApiKeyForHeaders: mockGetKey,
|
|
304
757
|
});
|
|
758
|
+
expect(state.provider).toBe("prompt_api");
|
|
759
|
+
expect(mockDefaultSetConfig).toHaveBeenCalledWith(
|
|
760
|
+
mockDb,
|
|
761
|
+
"provider",
|
|
762
|
+
"prompt_api",
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("polls transformers progress and emits events across status branches", async () => {
|
|
767
|
+
const state = makeState();
|
|
768
|
+
const mockEvents = {
|
|
769
|
+
emit: jest.fn<(...args: unknown[]) => unknown>(),
|
|
770
|
+
} as unknown as EventBus;
|
|
771
|
+
const mockStopPolling = jest.fn();
|
|
772
|
+
|
|
773
|
+
// Progress > 1 normalizes by / 100
|
|
774
|
+
global.fetch = jest
|
|
775
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
776
|
+
.mockResolvedValue({
|
|
777
|
+
ok: true,
|
|
778
|
+
json: async () => ({
|
|
779
|
+
progress: 75,
|
|
780
|
+
message: "Downloading weights...",
|
|
781
|
+
status: "running",
|
|
782
|
+
}),
|
|
783
|
+
} as unknown as Response) as typeof global.fetch;
|
|
305
784
|
|
|
306
785
|
await pollTransformersProgress(
|
|
307
786
|
state,
|
|
@@ -318,14 +797,16 @@ describe("provider operations", () => {
|
|
|
318
797
|
});
|
|
319
798
|
expect(mockStopPolling).not.toHaveBeenCalled();
|
|
320
799
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
800
|
+
// Progress <= 1 and status 'error'
|
|
801
|
+
global.fetch = jest
|
|
802
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
803
|
+
.mockResolvedValue({
|
|
804
|
+
ok: true,
|
|
805
|
+
json: async () => ({
|
|
806
|
+
progress: 0.4,
|
|
807
|
+
status: "error",
|
|
808
|
+
}),
|
|
809
|
+
} as unknown as Response) as typeof global.fetch;
|
|
329
810
|
|
|
330
811
|
await pollTransformersProgress(
|
|
331
812
|
state,
|
|
@@ -334,33 +815,130 @@ describe("provider operations", () => {
|
|
|
334
815
|
mockStopPolling,
|
|
335
816
|
);
|
|
336
817
|
expect(mockStopPolling).toHaveBeenCalledWith("group-p");
|
|
818
|
+
|
|
819
|
+
// Progress not finite
|
|
820
|
+
mockStopPolling.mockClear();
|
|
821
|
+
global.fetch = jest
|
|
822
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
823
|
+
.mockResolvedValue({
|
|
824
|
+
ok: true,
|
|
825
|
+
json: async () => ({
|
|
826
|
+
progress: "not-a-number",
|
|
827
|
+
status: "running",
|
|
828
|
+
}),
|
|
829
|
+
} as unknown as Response) as typeof global.fetch;
|
|
830
|
+
|
|
831
|
+
await pollTransformersProgress(
|
|
832
|
+
state,
|
|
833
|
+
mockEvents,
|
|
834
|
+
"group-p",
|
|
835
|
+
mockStopPolling,
|
|
836
|
+
);
|
|
837
|
+
expect(mockEvents.emit).toHaveBeenCalledWith(
|
|
838
|
+
"model-download-progress",
|
|
839
|
+
expect.objectContaining({ progress: null }),
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
// Res not ok
|
|
843
|
+
global.fetch = jest
|
|
844
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
845
|
+
.mockResolvedValue({
|
|
846
|
+
ok: false,
|
|
847
|
+
} as unknown as Response) as typeof global.fetch;
|
|
848
|
+
|
|
849
|
+
await pollTransformersProgress(
|
|
850
|
+
state,
|
|
851
|
+
mockEvents,
|
|
852
|
+
"group-p",
|
|
853
|
+
mockStopPolling,
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
// Fetch rejection handled cleanly
|
|
857
|
+
global.fetch = jest
|
|
858
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
859
|
+
.mockRejectedValue(new Error("Network failure")) as typeof global.fetch;
|
|
860
|
+
|
|
861
|
+
await pollTransformersProgress(
|
|
862
|
+
state,
|
|
863
|
+
mockEvents,
|
|
864
|
+
"group-p",
|
|
865
|
+
mockStopPolling,
|
|
866
|
+
);
|
|
337
867
|
});
|
|
338
868
|
|
|
339
|
-
it("starts and stops transformers progress polling", () => {
|
|
869
|
+
it("starts and stops transformers progress polling with timer interval", async () => {
|
|
340
870
|
jest.useFakeTimers();
|
|
341
871
|
const state = makeState();
|
|
342
|
-
const mockEvents
|
|
872
|
+
const mockEvents = {
|
|
873
|
+
emit: jest.fn<(...args: unknown[]) => unknown>(),
|
|
874
|
+
} as unknown as EventBus;
|
|
875
|
+
|
|
876
|
+
// Case 1: Initial poll finishes immediately with "done" (exercising line 595 callback)
|
|
877
|
+
global.fetch = jest
|
|
878
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
879
|
+
.mockResolvedValue({
|
|
880
|
+
ok: true,
|
|
881
|
+
json: async () => ({ progress: 100, status: "done" }),
|
|
882
|
+
} as unknown as Response) as typeof global.fetch;
|
|
883
|
+
|
|
884
|
+
startTransformersProgressPolling(state, mockEvents, "group-immediate");
|
|
885
|
+
await Promise.resolve();
|
|
886
|
+
await Promise.resolve();
|
|
887
|
+
expect(state.transformersProgressPollers.has("group-immediate")).toBe(
|
|
888
|
+
false,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Case 2: Initial poll running, interval finishes with "error" (exercising line 600 callback)
|
|
892
|
+
let callCount = 0;
|
|
893
|
+
global.fetch = jest
|
|
894
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
895
|
+
.mockImplementation(async () => {
|
|
896
|
+
callCount++;
|
|
897
|
+
return {
|
|
898
|
+
ok: true,
|
|
899
|
+
json: async () => ({
|
|
900
|
+
progress: callCount === 1 ? 50 : 100,
|
|
901
|
+
status: callCount === 1 ? "running" : "error",
|
|
902
|
+
}),
|
|
903
|
+
} as unknown as Response;
|
|
904
|
+
}) as typeof global.fetch;
|
|
343
905
|
|
|
344
906
|
startTransformersProgressPolling(state, mockEvents, "group-timer");
|
|
907
|
+
await Promise.resolve();
|
|
908
|
+
await Promise.resolve();
|
|
345
909
|
expect(state.transformersProgressPollers.has("group-timer")).toBe(true);
|
|
346
910
|
|
|
347
|
-
|
|
911
|
+
// Advance timer to trigger interval callback and resolve completion
|
|
912
|
+
await jest.advanceTimersByTimeAsync(1000);
|
|
348
913
|
expect(state.transformersProgressPollers.has("group-timer")).toBe(false);
|
|
914
|
+
|
|
915
|
+
// Calling stop when already stopped is a no-op
|
|
916
|
+
stopTransformersProgressPolling(state, "group-timer");
|
|
917
|
+
|
|
349
918
|
jest.useRealTimers();
|
|
350
919
|
});
|
|
351
920
|
|
|
352
|
-
it("cancels llamafile request via fetch", async () => {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
921
|
+
it("cancels llamafile request via fetch and handles failure cleanly", async () => {
|
|
922
|
+
global.fetch = jest
|
|
923
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
924
|
+
.mockResolvedValue({
|
|
925
|
+
ok: true,
|
|
926
|
+
} as unknown as Response) as typeof global.fetch;
|
|
356
927
|
|
|
357
928
|
await cancelLlamafileRequest("req-cancel-1");
|
|
358
|
-
expect(
|
|
929
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
359
930
|
expect.stringContaining("/cancel"),
|
|
360
931
|
expect.objectContaining({
|
|
361
932
|
method: "POST",
|
|
362
933
|
body: JSON.stringify({ requestId: "req-cancel-1" }),
|
|
363
934
|
}),
|
|
364
935
|
);
|
|
936
|
+
|
|
937
|
+
// Fetch rejection handled cleanly
|
|
938
|
+
global.fetch = jest
|
|
939
|
+
.fn<(...args: unknown[]) => Promise<unknown>>()
|
|
940
|
+
.mockRejectedValue(new Error("Cancel failed")) as typeof global.fetch;
|
|
941
|
+
|
|
942
|
+
await cancelLlamafileRequest("req-cancel-fail");
|
|
365
943
|
});
|
|
366
944
|
});
|