shadow-claw 1.52.0 → 1.52.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 +1 -1
- package/.well-known/mcp.json +1 -1
- package/README.md +9 -9
- package/dist/cli/build/build.js +1 -1
- package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
- package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
- package/dist/cli/cli.js +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
- package/dist/public/.well-known/mcp/server-card.json +1 -1
- package/dist/public/.well-known/mcp.json +1 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/README.md +9 -9
- package/dist/public/docs/README.md +1 -1
- package/dist/public/docs/architecture/orchestrator.md +2 -2
- package/dist/public/docs/architecture/overview.md +3 -3
- package/dist/public/docs/architecture/worker-protocol.md +29 -29
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/example/article.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/publishing.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/skill-creator.html +1 -1
- package/dist/public/docs/subsystems/local-models.md +1 -1
- package/dist/public/docs/subsystems/providers.md +11 -4
- package/dist/public/e2e/README.md +2 -2
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/main/memory.html +1 -1
- package/dist/public/main.html +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
- package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
- package/package.json +1 -1
- package/src/cli/build/build.test.ts +95 -0
- package/src/cli/build/build.ts +5 -1
- package/src/cli/commands/agent.test.ts +134 -0
- package/src/cli/commands/backup.test.ts +85 -0
- package/src/cli/commands/clients.test.ts +32 -0
- package/src/cli/commands/mcp.test.ts +260 -1
- package/src/cli/commands/send-file.test.ts +73 -0
- package/src/cli/commands/send.test.ts +58 -0
- package/src/cli/commands/tasks.test.ts +78 -0
- package/src/cli/commands/webrtc-listen.test.ts +74 -0
- package/src/cli/tools/assert-version-bump.test.ts +30 -1
- package/src/cli/tools/file-search-replace.test.ts +33 -1
- package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
- package/src/cli/tools/touch-nojekyll.test.ts +12 -0
- package/src/cli/tools/version.test.ts +109 -5
- package/src/cli/utils/agent-core.test.ts +40 -0
- package/src/cli/utils/control-client.test.ts +239 -2
- package/src/cli/utils/load-workspace-config.test.ts +63 -0
- package/src/cli/utils/local-models.test.ts +98 -0
- package/src/cli/utils/progress-bar.test.ts +41 -2
- package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
- package/src/cli/utils/resolve-project-root.test.ts +17 -0
- package/src/cli/utils/stdin.test.ts +38 -0
- package/src/cli/utils/stdio.test.ts +51 -1
- package/src/cli/utils/suppress-warnings.test.ts +9 -0
- package/src/cli/utils/webrtc-control-client.test.ts +569 -0
- package/src/common/utils/frontmatter.test.ts +117 -0
- package/src/components/common/help/providers.test.ts +67 -0
- package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
- package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
- package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
- package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
- package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
- package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
- package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
- package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
- package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
- package/src/server/logger.test.ts +57 -0
- package/src/server/peer.test.ts +47 -0
- package/src/server/routes/gemini.test.ts +244 -0
- package/src/server/server-peer.test.ts +245 -11
- package/src/server/services/llamafile-manager.test.ts +668 -0
- package/src/share-target/pending-shares.test.ts +90 -0
- package/src/shell/fs.test.ts +125 -0
- package/src/shell/shell.test.ts +8 -0
- package/src/subsystems/git/credentials.test.ts +260 -0
- package/src/subsystems/git/git.test.ts +64 -0
- package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
- package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
- package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
- package/src/worker/tools/email/email.test.ts +358 -0
- package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
- package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
- package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
- package/src/worker/tools/fetch-url/fetch-url.test.ts +295 -0
|
@@ -104,6 +104,7 @@ const mockSetMaxIterations = jest.fn(async () => {});
|
|
|
104
104
|
const mockSetLlamafileSettings = jest.fn(async () => {});
|
|
105
105
|
const mockSetContextCompressionEnabled = jest.fn(async () => {});
|
|
106
106
|
const mockSetStreamingEnabled = jest.fn(async () => {});
|
|
107
|
+
const mockSetModel = jest.fn(async () => {});
|
|
107
108
|
|
|
108
109
|
jest.unstable_mockModule(
|
|
109
110
|
"../../../core/orchestrator/utils/operations/provider.js",
|
|
@@ -113,6 +114,8 @@ jest.unstable_mockModule(
|
|
|
113
114
|
id,
|
|
114
115
|
name: p.name,
|
|
115
116
|
models: p.models,
|
|
117
|
+
modelsUrl: p.modelsUrl,
|
|
118
|
+
headers: p.headers,
|
|
116
119
|
requiresApiKey: p.requiresApiKey,
|
|
117
120
|
})),
|
|
118
121
|
),
|
|
@@ -136,14 +139,14 @@ jest.unstable_mockModule(
|
|
|
136
139
|
setBedrockSettings: mockSetBedrockSettings,
|
|
137
140
|
setLlamafileSettings: mockSetLlamafileSettings,
|
|
138
141
|
setMeshLlmSettings: jest.fn(async () => {}),
|
|
139
|
-
setModel:
|
|
142
|
+
setModel: mockSetModel,
|
|
140
143
|
setAssistantName: jest.fn(async () => {}),
|
|
141
144
|
setPeerjsMyAlias: jest.fn(async () => {}),
|
|
142
145
|
setPeerjsPeerAliases: jest.fn(async () => {}),
|
|
143
146
|
autoActivateProfile: jest.fn(async () => {}),
|
|
144
147
|
stopTransformersProgressPolling: jest.fn(),
|
|
145
|
-
getApiKeyForHeaders: jest.fn(),
|
|
146
|
-
getApiKeyForRequest: jest.fn(),
|
|
148
|
+
getApiKeyForHeaders: jest.fn<any>().mockResolvedValue(""),
|
|
149
|
+
getApiKeyForRequest: jest.fn<any>().mockResolvedValue(""),
|
|
147
150
|
}),
|
|
148
151
|
);
|
|
149
152
|
|
|
@@ -192,11 +195,8 @@ jest.unstable_mockModule("../../../db/db.js", () => ({
|
|
|
192
195
|
|
|
193
196
|
const { orchestratorStore } = await import("../../../stores/orchestrator.js");
|
|
194
197
|
const { ShadowClawLlm } = await import("./shadow-claw-llm.js");
|
|
195
|
-
const {
|
|
196
|
-
|
|
197
|
-
showWarning: _showWarning,
|
|
198
|
-
showError,
|
|
199
|
-
} = await import("../../../ui/toast.js");
|
|
198
|
+
const { showSuccess, showWarning, showError } =
|
|
199
|
+
await import("../../../ui/toast.js");
|
|
200
200
|
const { getConfig } = await import("../../../db/getConfig.js");
|
|
201
201
|
const { setConfig } = await import("../../../db/setConfig.js");
|
|
202
202
|
|
|
@@ -1212,6 +1212,528 @@ describe("shadow-claw-llm", () => {
|
|
|
1212
1212
|
});
|
|
1213
1213
|
orch.getProvider.mockReturnValue("bedrock_proxy");
|
|
1214
1214
|
el.updateModelSelector();
|
|
1215
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1216
|
+
|
|
1217
|
+
document.body.removeChild(el);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it("handles dynamic model fetching for llamafile and bedrock_proxy (success and failure)", async () => {
|
|
1221
|
+
const el = new ShadowClawLlm();
|
|
1222
|
+
const orch = createOrchestratorStub() as any;
|
|
1223
|
+
orch.getProvider.mockReturnValue("llamafile");
|
|
1224
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
1225
|
+
el.orchestrator = orch;
|
|
1226
|
+
el.db = {} as any;
|
|
1227
|
+
document.body.appendChild(el);
|
|
1228
|
+
await el.connectedCallback();
|
|
1229
|
+
await el.render();
|
|
1230
|
+
|
|
1231
|
+
// Success response for llamafile
|
|
1232
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValue({
|
|
1233
|
+
ok: true,
|
|
1234
|
+
status: 200,
|
|
1235
|
+
json: async () => [{ id: "discovered-llama-1" }],
|
|
1236
|
+
});
|
|
1237
|
+
el.updateModelSelector();
|
|
1238
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1239
|
+
expect(el.llamafileDiscoveredModelIds).toContain("discovered-llama-1");
|
|
1240
|
+
|
|
1241
|
+
// Failure response for llamafile
|
|
1242
|
+
(global.fetch as any) = jest
|
|
1243
|
+
.fn<any>()
|
|
1244
|
+
.mockRejectedValue(new Error("Llamafile offline"));
|
|
1245
|
+
el.updateModelSelector();
|
|
1246
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1247
|
+
expect(el.llamafileModelLoadError).toBe("Llamafile offline");
|
|
1248
|
+
|
|
1249
|
+
// Dynamic fetch failure for provider with static fallback
|
|
1250
|
+
orch.getProvider.mockReturnValue("provider-a");
|
|
1251
|
+
(global.fetch as any) = jest
|
|
1252
|
+
.fn<any>()
|
|
1253
|
+
.mockRejectedValue(new Error("Network fail"));
|
|
1254
|
+
el.updateModelSelector();
|
|
1255
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1256
|
+
|
|
1257
|
+
document.body.removeChild(el);
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it("handles saveModel for llamafile with request restart", async () => {
|
|
1261
|
+
const el = new ShadowClawLlm();
|
|
1262
|
+
const orch = createOrchestratorStub() as any;
|
|
1263
|
+
orch.getProvider.mockReturnValue("llamafile");
|
|
1264
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
1265
|
+
el.orchestrator = orch;
|
|
1266
|
+
el.db = {} as any;
|
|
1267
|
+
document.body.appendChild(el);
|
|
1268
|
+
await el.connectedCallback();
|
|
1269
|
+
await el.render();
|
|
1270
|
+
|
|
1271
|
+
(orchestratorStore as any).restartCurrentRequest = jest
|
|
1272
|
+
.fn<any>()
|
|
1273
|
+
.mockResolvedValue(true);
|
|
1274
|
+
await el.saveModel();
|
|
1275
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1276
|
+
"Model saved, request restarted",
|
|
1277
|
+
3000,
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
(orchestratorStore as any).restartCurrentRequest.mockResolvedValueOnce(
|
|
1281
|
+
false,
|
|
1282
|
+
);
|
|
1283
|
+
await el.saveModel();
|
|
1284
|
+
expect(showSuccess).toHaveBeenCalledWith("Model saved", 3000);
|
|
1285
|
+
|
|
1286
|
+
// Save model error
|
|
1287
|
+
mockSetModel.mockRejectedValueOnce(new Error("Disk full"));
|
|
1288
|
+
await el.saveModel();
|
|
1289
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1290
|
+
"Error saving model: Disk full",
|
|
1291
|
+
6000,
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
document.body.removeChild(el);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it("handles validations for rate limits, subagent parallel, workspace mode, and reasoning effort", async () => {
|
|
1298
|
+
const el = new ShadowClawLlm();
|
|
1299
|
+
const orch = createOrchestratorStub();
|
|
1300
|
+
el.orchestrator = orch as any;
|
|
1301
|
+
el.db = {} as any;
|
|
1302
|
+
document.body.appendChild(el);
|
|
1303
|
+
await el.connectedCallback();
|
|
1304
|
+
await el.render();
|
|
1305
|
+
|
|
1306
|
+
// Rate limit input validation
|
|
1307
|
+
const callsInput = el.shadowRoot?.querySelector(
|
|
1308
|
+
'[data-setting="rate-limit-calls-per-minute-input"]',
|
|
1309
|
+
) as HTMLInputElement;
|
|
1310
|
+
if (callsInput) {
|
|
1311
|
+
callsInput.value = "-5";
|
|
1312
|
+
await el.saveRateLimitSettings();
|
|
1313
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1314
|
+
"Please enter a valid non-negative calls-per-minute value",
|
|
1315
|
+
3000,
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
callsInput.value = "10";
|
|
1319
|
+
await el.saveRateLimitSettings();
|
|
1320
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1321
|
+
"Rate limit settings saved",
|
|
1322
|
+
3000,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Subagent max parallel validation
|
|
1327
|
+
const parallelInput = el.shadowRoot?.querySelector(
|
|
1328
|
+
'[data-setting="subagent-max-parallel-input"]',
|
|
1329
|
+
) as HTMLInputElement;
|
|
1330
|
+
if (parallelInput) {
|
|
1331
|
+
parallelInput.value = "0";
|
|
1332
|
+
await el.saveSubagentMaxParallel();
|
|
1333
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1334
|
+
"Please enter a valid number (1 or higher)",
|
|
1335
|
+
3000,
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
parallelInput.value = "8";
|
|
1339
|
+
await el.saveSubagentMaxParallel();
|
|
1340
|
+
expect(showSuccess).toHaveBeenCalledWith("Subagent limit saved", 3000);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Subagent workspace mode validation
|
|
1344
|
+
const wsModeSelect = el.shadowRoot?.querySelector(
|
|
1345
|
+
'[data-setting="subagent-workspace-mode-select"]',
|
|
1346
|
+
) as HTMLSelectElement;
|
|
1347
|
+
if (wsModeSelect) {
|
|
1348
|
+
wsModeSelect.value = "invalid-mode";
|
|
1349
|
+
await el.saveSubagentWorkspaceMode();
|
|
1350
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1351
|
+
"Please select a valid subagent workspace mode",
|
|
1352
|
+
3000,
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
wsModeSelect.value = "parent";
|
|
1356
|
+
await el.saveSubagentWorkspaceMode();
|
|
1357
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1358
|
+
"Subagent workspace mode saved",
|
|
1359
|
+
3000,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Null db branches for subagents
|
|
1364
|
+
el.db = null;
|
|
1365
|
+
await el.saveSubagentMaxParallel();
|
|
1366
|
+
expect(showError).toHaveBeenCalledWith("Database not initialized", 3000);
|
|
1367
|
+
|
|
1368
|
+
await el.saveSubagentWorkspaceMode();
|
|
1369
|
+
expect(showError).toHaveBeenCalledWith("Database not initialized", 3000);
|
|
1370
|
+
|
|
1371
|
+
// Reasoning effort error path
|
|
1372
|
+
el.db = {} as any;
|
|
1373
|
+
mockSetReasoningEffort.mockRejectedValueOnce(new Error("Effort invalid"));
|
|
1374
|
+
await el.saveReasoningEffort();
|
|
1375
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1376
|
+
"Error saving reasoning effort: Effort invalid",
|
|
1377
|
+
6000,
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
document.body.removeChild(el);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it("handles UI event listeners for buttons and toggles", async () => {
|
|
1384
|
+
const el = new ShadowClawLlm();
|
|
1385
|
+
const orch = createOrchestratorStub();
|
|
1386
|
+
el.orchestrator = orch as any;
|
|
1387
|
+
el.db = {} as any;
|
|
1388
|
+
document.body.appendChild(el);
|
|
1389
|
+
await el.connectedCallback();
|
|
1390
|
+
await el.render();
|
|
1391
|
+
|
|
1392
|
+
// Click save-model
|
|
1393
|
+
const saveModelBtn = el.shadowRoot?.querySelector(
|
|
1394
|
+
'[data-action="save-model"]',
|
|
1395
|
+
) as HTMLElement;
|
|
1396
|
+
saveModelBtn?.click();
|
|
1397
|
+
|
|
1398
|
+
// Click refresh-models
|
|
1399
|
+
const refreshBtn = el.shadowRoot?.querySelector(
|
|
1400
|
+
'[data-action="refresh-models"]',
|
|
1401
|
+
) as HTMLElement;
|
|
1402
|
+
refreshBtn?.click();
|
|
1403
|
+
|
|
1404
|
+
// Click save-rate-limit-settings
|
|
1405
|
+
const saveRateBtn = el.shadowRoot?.querySelector(
|
|
1406
|
+
'[data-action="save-rate-limit-settings"]',
|
|
1407
|
+
) as HTMLElement;
|
|
1408
|
+
saveRateBtn?.click();
|
|
1409
|
+
|
|
1410
|
+
// Click save-subagent-max-parallel
|
|
1411
|
+
const saveParallelBtn = el.shadowRoot?.querySelector(
|
|
1412
|
+
'[data-action="save-subagent-max-parallel"]',
|
|
1413
|
+
) as HTMLElement;
|
|
1414
|
+
saveParallelBtn?.click();
|
|
1415
|
+
|
|
1416
|
+
// Click save-subagent-workspace-mode
|
|
1417
|
+
const saveWsBtn = el.shadowRoot?.querySelector(
|
|
1418
|
+
'[data-action="save-subagent-workspace-mode"]',
|
|
1419
|
+
) as HTMLElement;
|
|
1420
|
+
saveWsBtn?.click();
|
|
1421
|
+
|
|
1422
|
+
// Click save-reasoning-effort
|
|
1423
|
+
const saveReasoningBtn = el.shadowRoot?.querySelector(
|
|
1424
|
+
'[data-action="save-reasoning-effort"]',
|
|
1425
|
+
) as HTMLElement;
|
|
1426
|
+
saveReasoningBtn?.click();
|
|
1427
|
+
|
|
1428
|
+
// Model select change to __custom__ shows custom input
|
|
1429
|
+
const modelSelect = el.shadowRoot?.querySelector(
|
|
1430
|
+
'[data-setting="model-select"]',
|
|
1431
|
+
) as HTMLSelectElement;
|
|
1432
|
+
const customInput = el.shadowRoot?.querySelector(
|
|
1433
|
+
'[data-setting="custom-model-input"]',
|
|
1434
|
+
) as HTMLElement;
|
|
1435
|
+
if (modelSelect && customInput) {
|
|
1436
|
+
const optCustom = document.createElement("option");
|
|
1437
|
+
optCustom.value = "__custom__";
|
|
1438
|
+
modelSelect.appendChild(optCustom);
|
|
1439
|
+
modelSelect.value = "__custom__";
|
|
1440
|
+
modelSelect.dispatchEvent(new Event("change"));
|
|
1441
|
+
expect(customInput.style.display).toBe("block");
|
|
1442
|
+
|
|
1443
|
+
const optModel = document.createElement("option");
|
|
1444
|
+
optModel.value = "model-a";
|
|
1445
|
+
modelSelect.appendChild(optModel);
|
|
1446
|
+
modelSelect.value = "model-a";
|
|
1447
|
+
modelSelect.dispatchEvent(new Event("change"));
|
|
1448
|
+
expect(customInput.style.display).toBe("none");
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Streaming toggle
|
|
1452
|
+
const streamingToggle = el.shadowRoot?.querySelector(
|
|
1453
|
+
'[data-setting="streaming-toggle"]',
|
|
1454
|
+
) as HTMLInputElement;
|
|
1455
|
+
if (streamingToggle) {
|
|
1456
|
+
streamingToggle.checked = false;
|
|
1457
|
+
streamingToggle.dispatchEvent(new Event("change"));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Context compression toggle
|
|
1461
|
+
const ctxToggle = el.shadowRoot?.querySelector(
|
|
1462
|
+
'[data-setting="context-compression-toggle"]',
|
|
1463
|
+
) as HTMLInputElement;
|
|
1464
|
+
if (ctxToggle) {
|
|
1465
|
+
ctxToggle.checked = true;
|
|
1466
|
+
ctxToggle.dispatchEvent(new Event("change"));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Prompt API fallback select change
|
|
1470
|
+
const fallbackSelect = el.shadowRoot?.querySelector(
|
|
1471
|
+
'[data-setting="prompt-api-fallback-select"]',
|
|
1472
|
+
) as HTMLSelectElement;
|
|
1473
|
+
if (fallbackSelect) {
|
|
1474
|
+
fallbackSelect.value = "test-model";
|
|
1475
|
+
fallbackSelect.dispatchEvent(new Event("change"));
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Repeated showLlamafileHelpDialog
|
|
1479
|
+
el.requestAppDialog = jest.fn<any>().mockResolvedValue(undefined);
|
|
1480
|
+
await el.showLlamafileHelpDialog("same-reason");
|
|
1481
|
+
await el.showLlamafileHelpDialog("same-reason");
|
|
1482
|
+
expect(el.requestAppDialog).toHaveBeenCalledTimes(1);
|
|
1483
|
+
|
|
1484
|
+
document.body.removeChild(el);
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
it("handles edge cases and error handling in saveApiKey, Bedrock, Llamafile, and BuiltinAi", async () => {
|
|
1488
|
+
const el = new ShadowClawLlm();
|
|
1489
|
+
const orch = createOrchestratorStub();
|
|
1490
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
1491
|
+
el.orchestrator = orch as any;
|
|
1492
|
+
el.db = {} as any;
|
|
1493
|
+
document.body.appendChild(el);
|
|
1494
|
+
await el.connectedCallback();
|
|
1495
|
+
await el.render();
|
|
1496
|
+
|
|
1497
|
+
// 1. saveApiKey without required key (provider-b)
|
|
1498
|
+
const providerSelect = el.shadowRoot?.querySelector(
|
|
1499
|
+
'[data-setting="provider-select"]',
|
|
1500
|
+
) as HTMLSelectElement;
|
|
1501
|
+
const keyInput = el.shadowRoot?.querySelector(
|
|
1502
|
+
'[data-setting="api-key-input"]',
|
|
1503
|
+
) as HTMLInputElement;
|
|
1504
|
+
|
|
1505
|
+
providerSelect.value = "provider-b";
|
|
1506
|
+
await el.saveApiKey();
|
|
1507
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1508
|
+
"Provider saved (no API key required)",
|
|
1509
|
+
3000,
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
// saveApiKey error path for provider-b
|
|
1513
|
+
mockSetProvider.mockRejectedValueOnce(new Error("Set provider crash"));
|
|
1514
|
+
await el.saveApiKey();
|
|
1515
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1516
|
+
"Error saving provider: Set provider crash",
|
|
1517
|
+
6000,
|
|
1518
|
+
);
|
|
1519
|
+
|
|
1520
|
+
// saveApiKey requiring key with empty key
|
|
1521
|
+
providerSelect.value = "provider-a";
|
|
1522
|
+
keyInput.value = " ";
|
|
1523
|
+
await el.saveApiKey();
|
|
1524
|
+
expect(showWarning).toHaveBeenCalledWith("Please enter an API key", 3000);
|
|
1525
|
+
|
|
1526
|
+
// saveApiKey with null orchestrator
|
|
1527
|
+
el.orchestrator = null;
|
|
1528
|
+
await el.saveApiKey();
|
|
1529
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1530
|
+
"Orchestrator not initialized",
|
|
1531
|
+
5000,
|
|
1532
|
+
);
|
|
1533
|
+
el.orchestrator = orch as any;
|
|
1534
|
+
|
|
1535
|
+
// 2. saveBedrockSettings validation & error
|
|
1536
|
+
const regionInput = el.shadowRoot?.querySelector(
|
|
1537
|
+
'[data-setting="bedrock-region-input"]',
|
|
1538
|
+
) as HTMLInputElement;
|
|
1539
|
+
const profileInput = el.shadowRoot?.querySelector(
|
|
1540
|
+
'[data-setting="bedrock-profile-input"]',
|
|
1541
|
+
) as HTMLInputElement;
|
|
1542
|
+
|
|
1543
|
+
if (regionInput && profileInput) {
|
|
1544
|
+
regionInput.value = "us-west-2";
|
|
1545
|
+
profileInput.value = "";
|
|
1546
|
+
await el.saveBedrockSettings();
|
|
1547
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1548
|
+
expect.stringContaining("Enter both Bedrock region and profile"),
|
|
1549
|
+
4000,
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
regionInput.value = "us-west-2";
|
|
1553
|
+
profileInput.value = "default";
|
|
1554
|
+
mockSetBedrockSettings.mockRejectedValueOnce(
|
|
1555
|
+
new Error("Bedrock auth fail"),
|
|
1556
|
+
);
|
|
1557
|
+
await el.saveBedrockSettings();
|
|
1558
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1559
|
+
"Error saving Bedrock fallback settings: Bedrock auth fail",
|
|
1560
|
+
6000,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// 3. saveBuiltinAiSettings error
|
|
1565
|
+
(setConfig as any).mockRejectedValueOnce(new Error("DB locked"));
|
|
1566
|
+
await el.saveBuiltinAiSettings();
|
|
1567
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1568
|
+
"Error saving Built-in AI settings: DB locked",
|
|
1569
|
+
5000,
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
// 4. saveLlamafileSettings validations & error
|
|
1573
|
+
const hostInput = el.shadowRoot?.querySelector(
|
|
1574
|
+
'[data-setting="llamafile-host-input"]',
|
|
1575
|
+
) as HTMLInputElement;
|
|
1576
|
+
const portInput = el.shadowRoot?.querySelector(
|
|
1577
|
+
'[data-setting="llamafile-port-input"]',
|
|
1578
|
+
) as HTMLInputElement;
|
|
1579
|
+
|
|
1580
|
+
if (hostInput && portInput) {
|
|
1581
|
+
hostInput.value = "";
|
|
1582
|
+
await el.saveLlamafileSettings();
|
|
1583
|
+
expect(showWarning).toHaveBeenCalledWith("Please enter a host", 3000);
|
|
1584
|
+
|
|
1585
|
+
hostInput.value = "localhost";
|
|
1586
|
+
portInput.value = "999999";
|
|
1587
|
+
await el.saveLlamafileSettings();
|
|
1588
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1589
|
+
"Please enter a valid port (1-65535)",
|
|
1590
|
+
3000,
|
|
1591
|
+
);
|
|
1592
|
+
|
|
1593
|
+
portInput.value = "8080";
|
|
1594
|
+
mockSetLlamafileSettings.mockRejectedValueOnce(new Error("Host down"));
|
|
1595
|
+
await el.saveLlamafileSettings();
|
|
1596
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1597
|
+
"Error saving llamafile settings: Host down",
|
|
1598
|
+
6000,
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// 5. saveMaxIterations
|
|
1603
|
+
const maxIterInput = el.shadowRoot?.querySelector(
|
|
1604
|
+
'[data-setting="max-iterations-input"]',
|
|
1605
|
+
) as HTMLInputElement;
|
|
1606
|
+
if (maxIterInput) {
|
|
1607
|
+
maxIterInput.value = "0";
|
|
1608
|
+
await el.saveMaxIterations();
|
|
1609
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1610
|
+
"Please enter a valid number (1 or higher)",
|
|
1611
|
+
3000,
|
|
1612
|
+
);
|
|
1613
|
+
|
|
1614
|
+
maxIterInput.value = "10";
|
|
1615
|
+
await el.saveMaxIterations();
|
|
1616
|
+
expect(mockSetMaxIterations).toHaveBeenCalledWith(
|
|
1617
|
+
expect.anything(),
|
|
1618
|
+
expect.anything(),
|
|
1619
|
+
10,
|
|
1620
|
+
);
|
|
1621
|
+
|
|
1622
|
+
mockSetMaxIterations.mockRejectedValueOnce(new Error("Save failed"));
|
|
1623
|
+
await el.saveMaxIterations();
|
|
1624
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1625
|
+
"Error saving max iterations: Save failed",
|
|
1626
|
+
6000,
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// 6. saveMaxTokens
|
|
1631
|
+
const maxTokensInput = el.shadowRoot?.querySelector(
|
|
1632
|
+
'[data-setting="max-tokens-input"]',
|
|
1633
|
+
) as HTMLInputElement;
|
|
1634
|
+
if (maxTokensInput) {
|
|
1635
|
+
maxTokensInput.value = "0";
|
|
1636
|
+
await el.saveMaxTokens();
|
|
1637
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1638
|
+
"Please enter a valid number (1 or higher)",
|
|
1639
|
+
3000,
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
maxTokensInput.value = "4096";
|
|
1643
|
+
await el.saveMaxTokens();
|
|
1644
|
+
expect(mockSetMaxTokens).toHaveBeenCalledWith(
|
|
1645
|
+
expect.anything(),
|
|
1646
|
+
expect.anything(),
|
|
1647
|
+
4096,
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
mockSetMaxTokens.mockRejectedValueOnce(new Error("Token save failed"));
|
|
1651
|
+
await el.saveMaxTokens();
|
|
1652
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1653
|
+
"Error saving max tokens: Token save failed",
|
|
1654
|
+
6000,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// 7. onStreamingToggle and onContextCompressionToggle
|
|
1659
|
+
await el.onStreamingToggle(true);
|
|
1660
|
+
expect(mockSetStreamingEnabled).toHaveBeenCalledWith(
|
|
1661
|
+
expect.anything(),
|
|
1662
|
+
expect.anything(),
|
|
1663
|
+
true,
|
|
1664
|
+
);
|
|
1665
|
+
await el.onStreamingToggle(false);
|
|
1666
|
+
expect(mockSetStreamingEnabled).toHaveBeenCalledWith(
|
|
1667
|
+
expect.anything(),
|
|
1668
|
+
expect.anything(),
|
|
1669
|
+
false,
|
|
1670
|
+
);
|
|
1671
|
+
mockSetStreamingEnabled.mockRejectedValueOnce(new Error("Stream err"));
|
|
1672
|
+
await el.onStreamingToggle(true);
|
|
1673
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1674
|
+
"Error saving streaming setting: Stream err",
|
|
1675
|
+
6000,
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1678
|
+
await el.onContextCompressionToggle(true);
|
|
1679
|
+
expect(mockSetContextCompressionEnabled).toHaveBeenCalledWith(
|
|
1680
|
+
expect.anything(),
|
|
1681
|
+
expect.anything(),
|
|
1682
|
+
true,
|
|
1683
|
+
);
|
|
1684
|
+
await el.onContextCompressionToggle(false);
|
|
1685
|
+
expect(mockSetContextCompressionEnabled).toHaveBeenCalledWith(
|
|
1686
|
+
expect.anything(),
|
|
1687
|
+
expect.anything(),
|
|
1688
|
+
false,
|
|
1689
|
+
);
|
|
1690
|
+
mockSetContextCompressionEnabled.mockRejectedValueOnce(
|
|
1691
|
+
new Error("Compress err"),
|
|
1692
|
+
);
|
|
1693
|
+
await el.onContextCompressionToggle(true);
|
|
1694
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1695
|
+
"Error saving context compression setting: Compress err",
|
|
1696
|
+
6000,
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
// 8. requestAppDialog fallback when shadow-claw element is absent
|
|
1700
|
+
const dialogFallback = await el.requestAppDialog({
|
|
1701
|
+
title: "T",
|
|
1702
|
+
message: "M",
|
|
1703
|
+
});
|
|
1704
|
+
expect(dialogFallback).toBe(false);
|
|
1705
|
+
|
|
1706
|
+
// 9. saveTransformersJsSettings and showLlamafileHelpDialog
|
|
1707
|
+
await el.saveTransformersJsSettings();
|
|
1708
|
+
await el.showLlamafileHelpDialog("help needed");
|
|
1709
|
+
// Repeating should trigger cache check:
|
|
1710
|
+
await el.showLlamafileHelpDialog("help needed");
|
|
1711
|
+
|
|
1712
|
+
// 10. saveSubagentWorkspaceMode invalid & error
|
|
1713
|
+
const subagentSelect = el.shadowRoot?.querySelector(
|
|
1714
|
+
'[data-setting="subagent-workspace-mode"], [data-setting="subagent-workspace-mode-select"]',
|
|
1715
|
+
) as HTMLSelectElement;
|
|
1716
|
+
if (subagentSelect) {
|
|
1717
|
+
subagentSelect.value = "invalid-mode";
|
|
1718
|
+
await el.saveSubagentWorkspaceMode();
|
|
1719
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1720
|
+
"Please select a valid subagent workspace mode",
|
|
1721
|
+
3000,
|
|
1722
|
+
);
|
|
1723
|
+
|
|
1724
|
+
subagentSelect.value = "parent";
|
|
1725
|
+
(setConfig as any).mockRejectedValueOnce(
|
|
1726
|
+
new Error("Subagent save error"),
|
|
1727
|
+
);
|
|
1728
|
+
await el.saveSubagentWorkspaceMode();
|
|
1729
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1730
|
+
"Error saving subagent workspace mode: Subagent save error",
|
|
1731
|
+
6000,
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// 11. saveMeshLlmSettings
|
|
1736
|
+
await el.saveMeshLlmSettings();
|
|
1215
1737
|
|
|
1216
1738
|
document.body.removeChild(el);
|
|
1217
1739
|
});
|