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.
Files changed (112) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +9 -9
  4. package/dist/cli/build/build.js +1 -1
  5. package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
  6. package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
  7. package/dist/cli/cli.js +1 -1
  8. package/dist/lib/components/index.js +1 -1
  9. package/dist/lib/components/shadow-claw-pages.js +1 -1
  10. package/dist/lib/index.js +1 -1
  11. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
  12. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
  13. package/dist/public/.well-known/mcp/server-card.json +1 -1
  14. package/dist/public/.well-known/mcp.json +1 -1
  15. package/dist/public/AGENTS.md +1 -1
  16. package/dist/public/README.md +9 -9
  17. package/dist/public/docs/README.md +1 -1
  18. package/dist/public/docs/architecture/orchestrator.md +2 -2
  19. package/dist/public/docs/architecture/overview.md +3 -3
  20. package/dist/public/docs/architecture/worker-protocol.md +29 -29
  21. package/dist/public/docs/example/article/index.html +1 -1
  22. package/dist/public/docs/example/article.html +1 -1
  23. package/dist/public/docs/publishing/index.html +1 -1
  24. package/dist/public/docs/publishing.html +1 -1
  25. package/dist/public/docs/skill-creator/index.html +1 -1
  26. package/dist/public/docs/skill-creator.html +1 -1
  27. package/dist/public/docs/subsystems/local-models.md +1 -1
  28. package/dist/public/docs/subsystems/providers.md +11 -4
  29. package/dist/public/e2e/README.md +2 -2
  30. package/dist/public/index.html +1 -1
  31. package/dist/public/index.js +1 -1
  32. package/dist/public/llms.txt +1 -1
  33. package/dist/public/main/index.html +1 -1
  34. package/dist/public/main/memory/index.html +1 -1
  35. package/dist/public/main/memory.html +1 -1
  36. package/dist/public/main.html +1 -1
  37. package/dist/public/service-worker.js +1 -1
  38. package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
  39. package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/build/build.test.ts +95 -0
  42. package/src/cli/build/build.ts +5 -1
  43. package/src/cli/commands/agent.test.ts +134 -0
  44. package/src/cli/commands/backup.test.ts +85 -0
  45. package/src/cli/commands/clients.test.ts +32 -0
  46. package/src/cli/commands/mcp.test.ts +260 -1
  47. package/src/cli/commands/send-file.test.ts +73 -0
  48. package/src/cli/commands/send.test.ts +58 -0
  49. package/src/cli/commands/tasks.test.ts +78 -0
  50. package/src/cli/commands/webrtc-listen.test.ts +74 -0
  51. package/src/cli/tools/assert-version-bump.test.ts +30 -1
  52. package/src/cli/tools/file-search-replace.test.ts +33 -1
  53. package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
  54. package/src/cli/tools/touch-nojekyll.test.ts +12 -0
  55. package/src/cli/tools/version.test.ts +109 -5
  56. package/src/cli/utils/agent-core.test.ts +40 -0
  57. package/src/cli/utils/control-client.test.ts +239 -2
  58. package/src/cli/utils/load-workspace-config.test.ts +63 -0
  59. package/src/cli/utils/local-models.test.ts +98 -0
  60. package/src/cli/utils/progress-bar.test.ts +41 -2
  61. package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
  62. package/src/cli/utils/resolve-project-root.test.ts +17 -0
  63. package/src/cli/utils/stdin.test.ts +38 -0
  64. package/src/cli/utils/stdio.test.ts +51 -1
  65. package/src/cli/utils/suppress-warnings.test.ts +9 -0
  66. package/src/cli/utils/webrtc-control-client.test.ts +569 -0
  67. package/src/common/utils/frontmatter.test.ts +117 -0
  68. package/src/components/common/help/providers.test.ts +67 -0
  69. package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
  70. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
  71. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
  72. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
  73. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
  74. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
  75. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
  76. package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
  77. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
  78. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
  79. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
  80. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
  81. package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
  82. package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
  83. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
  84. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
  85. package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
  86. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
  87. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
  88. package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
  89. package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
  90. package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
  91. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
  92. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
  93. package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
  94. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
  95. package/src/server/logger.test.ts +57 -0
  96. package/src/server/peer.test.ts +47 -0
  97. package/src/server/routes/gemini.test.ts +244 -0
  98. package/src/server/server-peer.test.ts +245 -11
  99. package/src/server/services/llamafile-manager.test.ts +668 -0
  100. package/src/share-target/pending-shares.test.ts +90 -0
  101. package/src/shell/fs.test.ts +125 -0
  102. package/src/shell/shell.test.ts +8 -0
  103. package/src/subsystems/git/credentials.test.ts +260 -0
  104. package/src/subsystems/git/git.test.ts +64 -0
  105. package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
  106. package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
  107. package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
  108. package/src/worker/tools/email/email.test.ts +358 -0
  109. package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
  110. package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
  111. package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
  112. 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: jest.fn(async () => {}),
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
- showSuccess,
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
  });