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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
let resolveUniqueAttachmentPath: any;
|
|
4
|
+
let mockGroupFileExists: any;
|
|
5
|
+
let mockUlid: any;
|
|
6
|
+
|
|
7
|
+
describe("resolveUniqueAttachmentPath", () => {
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
jest.resetModules();
|
|
10
|
+
mockGroupFileExists = jest.fn();
|
|
11
|
+
mockUlid = jest.fn(() => "mock-ulid-123");
|
|
12
|
+
|
|
13
|
+
jest.unstable_mockModule("../../../../storage/groupFileExists.js", () => ({
|
|
14
|
+
groupFileExists: mockGroupFileExists,
|
|
15
|
+
}));
|
|
16
|
+
jest.unstable_mockModule("../../../../utils/ulid.js", () => ({
|
|
17
|
+
ulid: mockUlid,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const mod = await import("./resolveUniqueAttachmentPath.js");
|
|
21
|
+
resolveUniqueAttachmentPath = mod.resolveUniqueAttachmentPath;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns base path when file does not exist", async () => {
|
|
25
|
+
mockGroupFileExists.mockResolvedValueOnce(false);
|
|
26
|
+
const result = await resolveUniqueAttachmentPath(
|
|
27
|
+
{} as any,
|
|
28
|
+
"group-1",
|
|
29
|
+
"downloads/email",
|
|
30
|
+
"file.pdf",
|
|
31
|
+
);
|
|
32
|
+
expect(result).toBe("downloads/email/file.pdf");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns incremented name when initial exists", async () => {
|
|
36
|
+
mockGroupFileExists
|
|
37
|
+
.mockResolvedValueOnce(true)
|
|
38
|
+
.mockResolvedValueOnce(false);
|
|
39
|
+
const result = await resolveUniqueAttachmentPath(
|
|
40
|
+
{} as any,
|
|
41
|
+
"group-1",
|
|
42
|
+
"downloads/email",
|
|
43
|
+
"file.pdf",
|
|
44
|
+
);
|
|
45
|
+
expect(result).toBe("downloads/email/file-2.pdf");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles empty directory", async () => {
|
|
49
|
+
mockGroupFileExists.mockResolvedValueOnce(false);
|
|
50
|
+
const result = await resolveUniqueAttachmentPath(
|
|
51
|
+
{} as any,
|
|
52
|
+
"group-1",
|
|
53
|
+
" ",
|
|
54
|
+
"file.txt",
|
|
55
|
+
);
|
|
56
|
+
expect(result).toBe("file.txt");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("falls back to ulid when all 1000 candidate names exist", async () => {
|
|
60
|
+
mockGroupFileExists.mockResolvedValue(true);
|
|
61
|
+
const result = await resolveUniqueAttachmentPath(
|
|
62
|
+
{} as any,
|
|
63
|
+
"group-1",
|
|
64
|
+
"downloads",
|
|
65
|
+
"file.txt",
|
|
66
|
+
);
|
|
67
|
+
expect(result).toBe("downloads/mock-ulid-123-file.txt");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to ulid when all 1000 candidate names exist in root directory", async () => {
|
|
71
|
+
mockGroupFileExists.mockResolvedValue(true);
|
|
72
|
+
const result = await resolveUniqueAttachmentPath(
|
|
73
|
+
{} as any,
|
|
74
|
+
"group-1",
|
|
75
|
+
"",
|
|
76
|
+
"file.txt",
|
|
77
|
+
);
|
|
78
|
+
expect(result).toBe("mock-ulid-123-file.txt");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -492,4 +492,115 @@ describe("worker/tools/fetch-file — bind safety", () => {
|
|
|
492
492
|
|
|
493
493
|
expect(result).toContain("Saved");
|
|
494
494
|
});
|
|
495
|
+
|
|
496
|
+
it("handles OAuth Git re-auth required on 401", async () => {
|
|
497
|
+
const deps = makeDeps({
|
|
498
|
+
fetchImpl: jest.fn(async () =>
|
|
499
|
+
createResponse({
|
|
500
|
+
status: 401,
|
|
501
|
+
statusText: "Unauthorized",
|
|
502
|
+
contentType: "text/plain",
|
|
503
|
+
body: "auth required",
|
|
504
|
+
}),
|
|
505
|
+
),
|
|
506
|
+
resolveGitCredentials: jest
|
|
507
|
+
.fn()
|
|
508
|
+
.mockResolvedValueOnce({
|
|
509
|
+
token: "old-token",
|
|
510
|
+
authMode: "oauth",
|
|
511
|
+
accountId: "git-1",
|
|
512
|
+
} as any)
|
|
513
|
+
.mockResolvedValueOnce({
|
|
514
|
+
reauthRequired: true,
|
|
515
|
+
provider: "github",
|
|
516
|
+
} as any),
|
|
517
|
+
buildAuthHeaders: jest.fn(() => ({ Authorization: "Bearer old-token" })),
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const result = await executeFetchFileTool(
|
|
521
|
+
{} as any,
|
|
522
|
+
{
|
|
523
|
+
url: "https://api.github.com/repos/org/repo/raw",
|
|
524
|
+
path: "out/file.txt",
|
|
525
|
+
use_git_auth: true,
|
|
526
|
+
},
|
|
527
|
+
"group-1",
|
|
528
|
+
deps,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(result).toContain("OAuth Git account reconnect required for github");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("refreshes OAuth Git token and retries on 401", async () => {
|
|
535
|
+
let callCount = 0;
|
|
536
|
+
const deps = makeDeps({
|
|
537
|
+
fetchImpl: jest.fn(async () => {
|
|
538
|
+
callCount++;
|
|
539
|
+
if (callCount === 1) {
|
|
540
|
+
return createResponse({
|
|
541
|
+
status: 401,
|
|
542
|
+
statusText: "Unauthorized",
|
|
543
|
+
contentType: "text/plain",
|
|
544
|
+
body: "unauthorized",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return createResponse({
|
|
548
|
+
status: 200,
|
|
549
|
+
statusText: "OK",
|
|
550
|
+
contentType: "text/plain",
|
|
551
|
+
body: "success content",
|
|
552
|
+
});
|
|
553
|
+
}),
|
|
554
|
+
resolveGitCredentials: jest
|
|
555
|
+
.fn()
|
|
556
|
+
.mockResolvedValueOnce({
|
|
557
|
+
token: "old-token",
|
|
558
|
+
authMode: "oauth",
|
|
559
|
+
accountId: "git-1",
|
|
560
|
+
} as any)
|
|
561
|
+
.mockResolvedValueOnce({
|
|
562
|
+
token: "new-token",
|
|
563
|
+
authMode: "oauth",
|
|
564
|
+
accountId: "git-1",
|
|
565
|
+
} as any),
|
|
566
|
+
buildAuthHeaders: jest
|
|
567
|
+
.fn()
|
|
568
|
+
.mockReturnValueOnce({ Authorization: "Bearer old-token" })
|
|
569
|
+
.mockReturnValueOnce({ Authorization: "Bearer new-token" }),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const result = await executeFetchFileTool(
|
|
573
|
+
{} as any,
|
|
574
|
+
{
|
|
575
|
+
url: "https://api.github.com/repos/org/repo/raw",
|
|
576
|
+
path: "out/file.txt",
|
|
577
|
+
use_git_auth: true,
|
|
578
|
+
},
|
|
579
|
+
"group-1",
|
|
580
|
+
deps,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
expect(deps.fetchImpl).toHaveBeenCalledTimes(2);
|
|
584
|
+
expect(result).toContain("Saved");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("handles generic network fetch errors", async () => {
|
|
588
|
+
const deps = makeDeps({
|
|
589
|
+
fetchImpl: jest.fn(async () => {
|
|
590
|
+
throw new Error("Connection refused");
|
|
591
|
+
}),
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const result = await executeFetchFileTool(
|
|
595
|
+
{} as any,
|
|
596
|
+
{ url: "https://unreachable.test", path: "out/file.txt" },
|
|
597
|
+
"group-1",
|
|
598
|
+
deps,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
expect(result).toContain(
|
|
602
|
+
"Network Error: Failed to fetch https://unreachable.test",
|
|
603
|
+
);
|
|
604
|
+
expect(result).toContain("Connection refused");
|
|
605
|
+
});
|
|
495
606
|
});
|
|
@@ -332,4 +332,299 @@ describe("worker/tools/fetch-url", () => {
|
|
|
332
332
|
expect(statusIndex).toBeGreaterThanOrEqual(0);
|
|
333
333
|
expect(beginIndex).toBeGreaterThan(statusIndex);
|
|
334
334
|
});
|
|
335
|
+
|
|
336
|
+
it("handles binary image and pdf responses and uploads to workspace", async () => {
|
|
337
|
+
const deps = makeDeps({
|
|
338
|
+
fetchImpl: jest.fn(async () => ({
|
|
339
|
+
...createResponse({
|
|
340
|
+
status: 200,
|
|
341
|
+
statusText: "OK",
|
|
342
|
+
contentType: "image/jpeg",
|
|
343
|
+
body: "image-bytes",
|
|
344
|
+
}),
|
|
345
|
+
headers: new Map([["content-type", "image/jpeg"]]),
|
|
346
|
+
})),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const imgResult = await executeFetchUrlTool(
|
|
350
|
+
{} as any,
|
|
351
|
+
{
|
|
352
|
+
url: "https://example.com/photo",
|
|
353
|
+
include_headers: true,
|
|
354
|
+
headers: { "X-Test": "1" },
|
|
355
|
+
},
|
|
356
|
+
"group-1",
|
|
357
|
+
deps,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(imgResult).toContain("Successfully downloaded binary file");
|
|
361
|
+
expect(imgResult).toContain("");
|
|
362
|
+
expect(deps.uploadGroupFile).toHaveBeenCalledWith(
|
|
363
|
+
{},
|
|
364
|
+
"group-1",
|
|
365
|
+
"downloads/photo.jpg",
|
|
366
|
+
expect.any(Blob),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// PDF binary
|
|
370
|
+
deps.fetchImpl = jest.fn(async () => ({
|
|
371
|
+
...createResponse({
|
|
372
|
+
status: 200,
|
|
373
|
+
statusText: "OK",
|
|
374
|
+
contentType: "application/pdf",
|
|
375
|
+
body: "pdf-bytes",
|
|
376
|
+
}),
|
|
377
|
+
headers: new Map([["content-type", "application/pdf"]]),
|
|
378
|
+
}));
|
|
379
|
+
|
|
380
|
+
const pdfResult = await executeFetchUrlTool(
|
|
381
|
+
{} as any,
|
|
382
|
+
{ url: "https://example.com/manual" },
|
|
383
|
+
"group-1",
|
|
384
|
+
deps,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(pdfResult).toContain("[Attachment](downloads/manual.pdf)");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("truncates long response bodies exceeding fetchMaxResponse", async () => {
|
|
391
|
+
const deps = makeDeps({
|
|
392
|
+
fetchMaxResponse: 20,
|
|
393
|
+
fetchImpl: jest.fn(async () =>
|
|
394
|
+
createResponse({
|
|
395
|
+
status: 200,
|
|
396
|
+
statusText: "OK",
|
|
397
|
+
contentType: "text/plain",
|
|
398
|
+
body: "01234567890123456789extra_content_here",
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const result = await executeFetchUrlTool(
|
|
404
|
+
{} as any,
|
|
405
|
+
{ url: "https://example.com/large" },
|
|
406
|
+
"group-1",
|
|
407
|
+
deps,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(result).toContain(
|
|
411
|
+
"--- Response truncated (showed 20 of 38 chars) ---",
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("handles git auth, reauth requirement, and 401 retry", async () => {
|
|
416
|
+
// 1. Initial reauthRequired
|
|
417
|
+
const deps1 = makeDeps({
|
|
418
|
+
resolveGitCredentials: jest.fn(async () => ({
|
|
419
|
+
token: undefined,
|
|
420
|
+
reauthRequired: true,
|
|
421
|
+
hostPattern: "github.com",
|
|
422
|
+
})),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const res1 = await executeFetchUrlTool(
|
|
426
|
+
{} as any,
|
|
427
|
+
{ url: "https://github.com/org/repo", use_git_auth: true },
|
|
428
|
+
"group-1",
|
|
429
|
+
deps1,
|
|
430
|
+
);
|
|
431
|
+
expect(res1).toContain(
|
|
432
|
+
"OAuth Git account reconnect required for github.com",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// 2. 401 on git auth with refreshed credentials needing reauth
|
|
436
|
+
const deps2 = makeDeps({
|
|
437
|
+
resolveGitCredentials: jest
|
|
438
|
+
.fn<any>()
|
|
439
|
+
.mockResolvedValueOnce({
|
|
440
|
+
token: "old-token",
|
|
441
|
+
authMode: "oauth",
|
|
442
|
+
hostPattern: "github.com",
|
|
443
|
+
})
|
|
444
|
+
.mockResolvedValueOnce({
|
|
445
|
+
token: undefined,
|
|
446
|
+
authMode: "oauth",
|
|
447
|
+
reauthRequired: true,
|
|
448
|
+
hostPattern: "github.com",
|
|
449
|
+
}),
|
|
450
|
+
buildAuthHeaders: jest.fn(() => ({ Authorization: "Bearer old-token" })),
|
|
451
|
+
fetchImpl: jest.fn(async () =>
|
|
452
|
+
createResponse({
|
|
453
|
+
status: 401,
|
|
454
|
+
statusText: "Unauthorized",
|
|
455
|
+
contentType: "text/plain",
|
|
456
|
+
body: "Bad credentials",
|
|
457
|
+
}),
|
|
458
|
+
),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const res2 = await executeFetchUrlTool(
|
|
462
|
+
{} as any,
|
|
463
|
+
{ url: "https://github.com/org/repo", use_git_auth: true },
|
|
464
|
+
"group-1",
|
|
465
|
+
deps2,
|
|
466
|
+
);
|
|
467
|
+
expect(res2).toContain(
|
|
468
|
+
"OAuth Git account reconnect required for github.com",
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// 3. 401 on git auth with headers updated succeeds on second try
|
|
472
|
+
const deps3 = makeDeps({
|
|
473
|
+
resolveGitCredentials: jest
|
|
474
|
+
.fn<any>()
|
|
475
|
+
.mockResolvedValueOnce({
|
|
476
|
+
token: "token1",
|
|
477
|
+
authMode: "oauth",
|
|
478
|
+
})
|
|
479
|
+
.mockResolvedValueOnce({
|
|
480
|
+
token: "token2",
|
|
481
|
+
authMode: "oauth",
|
|
482
|
+
}),
|
|
483
|
+
buildAuthHeaders: jest
|
|
484
|
+
.fn<any>()
|
|
485
|
+
.mockReturnValueOnce({ Authorization: "Bearer token1" })
|
|
486
|
+
.mockReturnValueOnce({ Authorization: "Bearer token2" }),
|
|
487
|
+
fetchImpl: jest
|
|
488
|
+
.fn<any>()
|
|
489
|
+
.mockResolvedValueOnce(
|
|
490
|
+
createResponse({
|
|
491
|
+
status: 401,
|
|
492
|
+
statusText: "Unauthorized",
|
|
493
|
+
contentType: "text/plain",
|
|
494
|
+
body: "stale",
|
|
495
|
+
}),
|
|
496
|
+
)
|
|
497
|
+
.mockResolvedValueOnce(
|
|
498
|
+
createResponse({
|
|
499
|
+
status: 200,
|
|
500
|
+
statusText: "OK",
|
|
501
|
+
contentType: "text/plain",
|
|
502
|
+
body: "refreshed git data",
|
|
503
|
+
}),
|
|
504
|
+
),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const res3 = await executeFetchUrlTool(
|
|
508
|
+
{} as any,
|
|
509
|
+
{ url: "https://github.com/org/repo", use_git_auth: true },
|
|
510
|
+
"group-1",
|
|
511
|
+
deps3,
|
|
512
|
+
);
|
|
513
|
+
expect(res3).toContain("refreshed git data");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("provides helpful hints on 401 and 404 responses", async () => {
|
|
517
|
+
// 401 on github without use_git_auth
|
|
518
|
+
const deps = makeDeps({
|
|
519
|
+
fetchImpl: jest.fn(async () =>
|
|
520
|
+
createResponse({
|
|
521
|
+
status: 401,
|
|
522
|
+
statusText: "Unauthorized",
|
|
523
|
+
contentType: "text/plain",
|
|
524
|
+
body: "Need auth",
|
|
525
|
+
}),
|
|
526
|
+
),
|
|
527
|
+
resolveServiceCredentials: jest.fn(
|
|
528
|
+
async () =>
|
|
529
|
+
({
|
|
530
|
+
service: "Slack",
|
|
531
|
+
hostPattern: "slack.com",
|
|
532
|
+
}) as any,
|
|
533
|
+
),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const gitHintRes = await executeFetchUrlTool(
|
|
537
|
+
{} as any,
|
|
538
|
+
{ url: "https://github.com/private/repo" },
|
|
539
|
+
"group-1",
|
|
540
|
+
deps,
|
|
541
|
+
);
|
|
542
|
+
expect(gitHintRes).toContain(
|
|
543
|
+
"This looks like a Git host and you received a 401",
|
|
544
|
+
);
|
|
545
|
+
expect(gitHintRes).toContain("use_git_auth: true");
|
|
546
|
+
|
|
547
|
+
// 401 on non-git host with saved account
|
|
548
|
+
const svcHintRes = await executeFetchUrlTool(
|
|
549
|
+
{} as any,
|
|
550
|
+
{ url: "https://slack.com/api/test" },
|
|
551
|
+
"group-1",
|
|
552
|
+
deps,
|
|
553
|
+
);
|
|
554
|
+
expect(svcHintRes).toContain(
|
|
555
|
+
'A saved account for "Slack" (slack.com) was found',
|
|
556
|
+
);
|
|
557
|
+
expect(svcHintRes).toContain("use_account_auth: true");
|
|
558
|
+
|
|
559
|
+
// Generic 404 error
|
|
560
|
+
deps.fetchImpl = jest.fn(async () =>
|
|
561
|
+
createResponse({
|
|
562
|
+
status: 404,
|
|
563
|
+
statusText: "Not Found",
|
|
564
|
+
contentType: "text/plain",
|
|
565
|
+
body: "Missing page",
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
const notFoundRes = await executeFetchUrlTool(
|
|
569
|
+
{} as any,
|
|
570
|
+
{ url: "https://example.com/notfound" },
|
|
571
|
+
"group-1",
|
|
572
|
+
deps,
|
|
573
|
+
);
|
|
574
|
+
expect(notFoundRes).toContain("[HTTP 404 Not Found]");
|
|
575
|
+
expect(notFoundRes).toContain(
|
|
576
|
+
"Error fetching URL. Content preview:\nMissing page",
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("handles retryable error toast and exceptions", async () => {
|
|
581
|
+
const deps = makeDeps({
|
|
582
|
+
withRetry: jest.fn(async (fn: any, options: any) => {
|
|
583
|
+
// Trigger onRetry callback
|
|
584
|
+
options.onRetry(1, 3, 1000, new Error("Temporary timeout"));
|
|
585
|
+
return fn();
|
|
586
|
+
}),
|
|
587
|
+
fetchImpl: jest.fn(async () => {
|
|
588
|
+
throw new TypeError("Failed to fetch");
|
|
589
|
+
}),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const networkErr = await executeFetchUrlTool(
|
|
593
|
+
{} as any,
|
|
594
|
+
{ url: "https://broken.domain" },
|
|
595
|
+
"group-1",
|
|
596
|
+
deps,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
expect(deps.post).toHaveBeenCalledWith(
|
|
600
|
+
expect.objectContaining({
|
|
601
|
+
type: "show-toast",
|
|
602
|
+
payload: expect.objectContaining({
|
|
603
|
+
message: expect.stringContaining("Temporary timeout"),
|
|
604
|
+
}),
|
|
605
|
+
}),
|
|
606
|
+
);
|
|
607
|
+
expect(networkErr).toContain(
|
|
608
|
+
"Network Error: Failed to fetch https://broken.domain",
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// HttpError instance thrown during fetch
|
|
612
|
+
const { HttpError } = await import("../utils/HttpError.js");
|
|
613
|
+
deps.fetchImpl = jest.fn(async () => {
|
|
614
|
+
throw new HttpError(
|
|
615
|
+
503,
|
|
616
|
+
"Service Unavailable",
|
|
617
|
+
"Over capacity",
|
|
618
|
+
"X-Retry-After: 5\n",
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
const httpErr = await executeFetchUrlTool(
|
|
622
|
+
{} as any,
|
|
623
|
+
{ url: "https://overloaded.domain" },
|
|
624
|
+
"group-1",
|
|
625
|
+
deps,
|
|
626
|
+
);
|
|
627
|
+
expect(httpErr).toContain("[HTTP 503 Service Unavailable]");
|
|
628
|
+
expect(httpErr).toContain("Over capacity");
|
|
629
|
+
});
|
|
335
630
|
});
|