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.
Files changed (86) hide show
  1. package/.well-known/mcp/server-card.json +2 -2
  2. package/.well-known/mcp/servers.json +1 -1
  3. package/.well-known/mcp.json +2 -2
  4. package/dist/cli/cli.js +2 -2
  5. package/dist/lib/components/index.js +2 -2
  6. package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
  7. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  8. package/dist/lib/components/shadow-claw.js +1 -1
  9. package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
  10. package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
  11. package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
  12. package/dist/lib/index.js +2 -2
  13. package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
  14. package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
  15. package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
  16. package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
  17. package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
  18. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
  19. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
  20. package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
  21. package/dist/public/.well-known/mcp/server-card.json +2 -2
  22. package/dist/public/.well-known/mcp/servers.json +1 -1
  23. package/dist/public/.well-known/mcp.json +2 -2
  24. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  25. package/dist/public/docs/example/article/index.html +2 -2
  26. package/dist/public/docs/publishing/index.html +2 -2
  27. package/dist/public/docs/skill-creator/index.html +2 -2
  28. package/dist/public/index.html +2 -2
  29. package/dist/public/index.js +1 -1
  30. package/dist/public/main/index.html +2 -2
  31. package/dist/public/main/memory/index.html +2 -2
  32. package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
  33. package/dist/public/service-worker.js +1 -1
  34. package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
  35. package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
  36. package/dist/server.js +1 -1
  37. package/package.json +1 -1
  38. package/src/cli/cli.ts +2 -2
  39. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
  40. package/src/core/effect.test.ts +19 -0
  41. package/src/core/orchestrator/orchestrator-state.ts +2 -1
  42. package/src/core/orchestrator/orchestrator.test.ts +686 -5
  43. package/src/core/orchestrator/orchestrator.ts +3 -10
  44. package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
  45. package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
  46. package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
  47. package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
  48. package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
  49. package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
  50. package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
  51. package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
  52. package/src/core/orchestrator/utils/initTasks.ts +5 -4
  53. package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
  54. package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
  55. package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
  56. package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
  57. package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
  58. package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
  59. package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
  60. package/src/core/orchestrator/utils/operations/task.ts +1 -1
  61. package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
  62. package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
  63. package/src/core/orchestrator/utils/settings.test.ts +248 -81
  64. package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
  65. package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
  66. package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
  67. package/src/server/routes/mcp.ts +2 -2
  68. package/src/stores/file-viewer.test.ts +11 -0
  69. package/src/stores/theme.test.ts +41 -2
  70. package/src/stores/toast.test.ts +25 -0
  71. package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
  72. package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
  73. package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
  74. package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
  75. package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
  76. package/src/worker/tools/node-transformers-executor.test.ts +105 -6
  77. package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
  78. package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
  79. package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
  80. package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
  81. package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
  82. package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
  83. package/src/worker/tools/tasks/update-task.test.ts +78 -14
  84. package/src/worker/tools/ui/javascript.test.ts +70 -17
  85. package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
  86. 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
- } from "./provider.js";
28
-
29
- import type { OrchestratorState } from "../../orchestrator-state.js";
109
+ } = await import("./provider.js");
30
110
 
31
- function makeState(overrides: Partial<OrchestratorState> = {}) {
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
- providerConfig: {
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
- } as any,
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
- transformersProgressPollers: new Map(),
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: any = {
55
- getApiKey: jest.fn().mockResolvedValue("sk-secret-123" as never),
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: any = {
65
- getApiKey: jest.fn().mockResolvedValue(null as never),
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 state2 = makeState({ reasoningEffort: "none" });
102
- expect(getReasoningConfig(state2)).toBeUndefined();
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
- const state3 = makeState({ reasoningEffort: "" as any });
105
- expect(getReasoningConfig(state3)).toBeUndefined();
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: { id: "llamafile", headers: {} } as any,
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: { id: "mesh-llm", headers: {} } as any,
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
- // Non-matching providerConfig does not throw
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: { id: "anthropic" } as any,
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
- } as any,
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
- } as any,
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.fn().mockResolvedValue(undefined as never);
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
- {} as any,
494
+ mockDb,
229
495
  { authMode: "sso", profile: "prof1", region: "us-west-2" },
230
- mockSetConfig as any,
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
- {} as any,
519
+ mockDb,
239
520
  { host: "192.168.1.1", mode: "cli", offline: true, port: 9999 },
240
- mockSetConfig as any,
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
- {} as any,
530
+ mockDb,
250
531
  { host: "https://mesh.new.host" },
251
- mockSetConfig as any,
532
+ mockSetConfig,
252
533
  );
253
534
  expect(state.meshLlmHost).toBe("https://mesh.new.host");
254
535
 
255
- await setModel(state, {} as any, "openrouter/free", mockSetConfig as any);
536
+ await setModel(state, mockDb, "openrouter/free", mockSetConfig);
256
537
  expect(state.model).toBe("openrouter/free");
257
538
 
258
- await setPeerjsMyAlias(state, {} as any, "my-alias", mockSetConfig as any);
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
- {} as any,
544
+ mockDb,
264
545
  { peer1: "Alias 1" },
265
- mockSetConfig as any,
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.fn().mockResolvedValue(undefined as never);
273
- const mockLoadKey = jest.fn().mockResolvedValue(undefined as never);
274
- const mockGetKey = jest.fn().mockResolvedValue("key123" as never);
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
- {} as any,
647
+ mockDb,
279
648
  "openrouter",
280
649
  {
281
- loadApiKeyForProvider: mockLoadKey as any,
282
- getApiKeyForHeaders: mockGetKey as any,
650
+ loadApiKeyForProvider: mockLoadKey,
651
+ getApiKeyForHeaders: mockGetKey,
283
652
  },
284
- mockSetConfig as any,
653
+ mockSetConfig,
285
654
  );
286
655
 
287
656
  expect(state.provider).toBe("openrouter");
288
- expect(mockLoadKey).toHaveBeenCalledWith({}, "openrouter");
289
- expect(mockSetConfig).toHaveBeenCalledWith({}, "provider", "openrouter");
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("polls transformers progress and emits events", async () => {
709
+ it("async setters work with default setConfig implementation", async () => {
293
710
  const state = makeState();
294
- const mockEvents: any = { emit: jest.fn() };
295
- const mockStopPolling = jest.fn();
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
- (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
298
- ok: true,
299
- json: async () => ({
300
- progress: 75,
301
- message: "Downloading weights...",
302
- status: "running",
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
- // When status is 'done', stops polling
322
- (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
323
- ok: true,
324
- json: async () => ({
325
- progress: 100,
326
- status: "done",
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: any = { emit: jest.fn() };
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
- stopTransformersProgressPolling(state, "group-timer");
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
- (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
354
- ok: true,
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(globalThis.fetch).toHaveBeenCalledWith(
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
  });