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
|
@@ -422,6 +422,146 @@ describe("LlamafileManagerService", () => {
|
|
|
422
422
|
expect(res.send).toHaveBeenCalledWith("Bad Gateway");
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
+
it("handles invokeLlamafileServer streaming success", async () => {
|
|
426
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
427
|
+
|
|
428
|
+
const req: any = { headers: {} };
|
|
429
|
+
const res: any = {
|
|
430
|
+
status: jest.fn().mockReturnThis(),
|
|
431
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
432
|
+
write: jest.fn().mockReturnThis(),
|
|
433
|
+
end: jest.fn().mockReturnThis(),
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const chunks = [
|
|
437
|
+
new TextEncoder().encode(
|
|
438
|
+
'data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n',
|
|
439
|
+
),
|
|
440
|
+
];
|
|
441
|
+
let readIndex = 0;
|
|
442
|
+
const mockReader = {
|
|
443
|
+
read: jest.fn(async () => {
|
|
444
|
+
if (readIndex < chunks.length) {
|
|
445
|
+
return { done: false, value: chunks[readIndex++] };
|
|
446
|
+
}
|
|
447
|
+
return { done: true, value: undefined };
|
|
448
|
+
}),
|
|
449
|
+
releaseLock: jest.fn(),
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValue({
|
|
453
|
+
ok: true,
|
|
454
|
+
status: 200,
|
|
455
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
456
|
+
body: {
|
|
457
|
+
getReader: () => mockReader,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
await invokeLlamafileServer(
|
|
462
|
+
req,
|
|
463
|
+
res,
|
|
464
|
+
{ model: "custom-model", messages: [], stream: true },
|
|
465
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
466
|
+
true,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
470
|
+
expect(res.write).toHaveBeenCalled();
|
|
471
|
+
expect(res.end).toHaveBeenCalled();
|
|
472
|
+
expect(mockReader.releaseLock).toHaveBeenCalled();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("handles invokeLlamafileServer non-streaming success", async () => {
|
|
476
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
477
|
+
|
|
478
|
+
const req: any = { headers: {} };
|
|
479
|
+
const res: any = {
|
|
480
|
+
status: jest.fn().mockReturnThis(),
|
|
481
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
482
|
+
send: jest.fn().mockReturnThis(),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const jsonStr = JSON.stringify({
|
|
486
|
+
choices: [{ message: { content: "Hello" } }],
|
|
487
|
+
});
|
|
488
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValue({
|
|
489
|
+
ok: true,
|
|
490
|
+
status: 200,
|
|
491
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
492
|
+
arrayBuffer: async () => Buffer.from(jsonStr),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await invokeLlamafileServer(
|
|
496
|
+
req,
|
|
497
|
+
res,
|
|
498
|
+
{ model: "custom-model", messages: [] },
|
|
499
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
500
|
+
false,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
504
|
+
expect(res.send).toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("handles invokeLlamafileServer fetch error when headers not sent", async () => {
|
|
508
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
509
|
+
|
|
510
|
+
const req: any = { headers: {} };
|
|
511
|
+
const res: any = {
|
|
512
|
+
headersSent: false,
|
|
513
|
+
status: jest.fn().mockReturnThis(),
|
|
514
|
+
json: jest.fn().mockReturnThis(),
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
(global.fetch as any) = jest
|
|
518
|
+
.fn<any>()
|
|
519
|
+
.mockRejectedValue(new Error("Connection refused"));
|
|
520
|
+
|
|
521
|
+
await invokeLlamafileServer(
|
|
522
|
+
req,
|
|
523
|
+
res,
|
|
524
|
+
{ model: "custom-model", messages: [] },
|
|
525
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
526
|
+
false,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(res.status).toHaveBeenCalledWith(502);
|
|
530
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
531
|
+
expect.objectContaining({
|
|
532
|
+
error: expect.stringContaining("Connection refused"),
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("handles invokeLlamafileServer fetch error when headers already sent", async () => {
|
|
538
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
539
|
+
|
|
540
|
+
const req: any = { headers: {} };
|
|
541
|
+
const res: any = {
|
|
542
|
+
headersSent: true,
|
|
543
|
+
write: jest.fn().mockReturnThis(),
|
|
544
|
+
end: jest.fn().mockReturnThis(),
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
(global.fetch as any) = jest
|
|
548
|
+
.fn<any>()
|
|
549
|
+
.mockRejectedValue(new Error("Mid-stream abort"));
|
|
550
|
+
|
|
551
|
+
await invokeLlamafileServer(
|
|
552
|
+
req,
|
|
553
|
+
res,
|
|
554
|
+
{ model: "custom-model", messages: [] },
|
|
555
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
556
|
+
false,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(res.write).toHaveBeenCalledWith(
|
|
560
|
+
expect.stringContaining("Mid-stream abort"),
|
|
561
|
+
);
|
|
562
|
+
expect(res.end).toHaveBeenCalled();
|
|
563
|
+
});
|
|
564
|
+
|
|
425
565
|
it("rejects immediately if request is already destroyed or aborted before spawn", async () => {
|
|
426
566
|
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
427
567
|
readdirMock.mockResolvedValue([
|
|
@@ -599,6 +739,65 @@ describe("LlamafileManagerService", () => {
|
|
|
599
739
|
);
|
|
600
740
|
expect(result).toEqual({ output: "2026-10-12T00:00:00Z" });
|
|
601
741
|
});
|
|
742
|
+
|
|
743
|
+
it("listTools extracts tools from object with tools or data property", async () => {
|
|
744
|
+
const service = await getService();
|
|
745
|
+
|
|
746
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
|
|
747
|
+
ok: true,
|
|
748
|
+
status: 200,
|
|
749
|
+
json: async () => ({ tools: [{ name: "t_prop" }] }),
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const res1 = await service.listTools({ mode: "server" });
|
|
753
|
+
expect(res1).toEqual([{ name: "t_prop" }]);
|
|
754
|
+
|
|
755
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
|
|
756
|
+
ok: true,
|
|
757
|
+
status: 200,
|
|
758
|
+
json: async () => ({ data: [{ name: "d_prop" }] }),
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const res2 = await service.listTools({ mode: "server" });
|
|
762
|
+
expect(res2).toEqual([{ name: "d_prop" }]);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("executeTool handles error from upstream in server mode", async () => {
|
|
766
|
+
const service = await getService();
|
|
767
|
+
|
|
768
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValueOnce({
|
|
769
|
+
ok: false,
|
|
770
|
+
status: 500,
|
|
771
|
+
text: async () => "Internal tool failure",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await expect(
|
|
775
|
+
service.executeTool("custom_tool", {}, { mode: "server" }),
|
|
776
|
+
).rejects.toThrow(
|
|
777
|
+
"Upstream /tools returned HTTP 500: Internal tool failure",
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("executeTool handles local get_info and unsupported tools in CLI mode", async () => {
|
|
782
|
+
const service = await getService();
|
|
783
|
+
|
|
784
|
+
const info = await service.executeTool("get_info", {}, { mode: "cli" });
|
|
785
|
+
expect(info.platform).toBe(process.platform);
|
|
786
|
+
expect(info.cwd).toBe(process.cwd());
|
|
787
|
+
|
|
788
|
+
await expect(
|
|
789
|
+
service.executeTool("unsupported_action", {}, { mode: "cli" }),
|
|
790
|
+
).rejects.toThrow(/requires server mode or is not supported locally/);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("terminateAll calls cancellation handlers and cleans up processes", async () => {
|
|
794
|
+
const service = await getService();
|
|
795
|
+
const cancelFn = jest.fn();
|
|
796
|
+
service.__getActiveRequests().set("test-cancel", cancelFn);
|
|
797
|
+
|
|
798
|
+
service.terminateAll();
|
|
799
|
+
expect(cancelFn).toHaveBeenCalled();
|
|
800
|
+
});
|
|
602
801
|
});
|
|
603
802
|
|
|
604
803
|
describe("buildCliPrompt with tools support", () => {
|
|
@@ -629,6 +828,162 @@ describe("LlamafileManagerService", () => {
|
|
|
629
828
|
expect(promptWithTools).toContain("USER: What is the time?");
|
|
630
829
|
expect(promptWithTools).toContain("ASSISTANT:");
|
|
631
830
|
});
|
|
831
|
+
|
|
832
|
+
it("handles complex array content in buildCliPrompt", async () => {
|
|
833
|
+
const { buildCliPrompt } = await import("./llamafile-manager.js");
|
|
834
|
+
const prompt = buildCliPrompt({
|
|
835
|
+
messages: [
|
|
836
|
+
{
|
|
837
|
+
role: "user",
|
|
838
|
+
content: [
|
|
839
|
+
"string block",
|
|
840
|
+
{ type: "text", text: "text block" },
|
|
841
|
+
{ type: "tool_result", content: "result content" },
|
|
842
|
+
{ type: "tool_use", name: "bash", input: { command: "ls" } },
|
|
843
|
+
null as any,
|
|
844
|
+
],
|
|
845
|
+
},
|
|
846
|
+
],
|
|
847
|
+
});
|
|
848
|
+
expect(prompt).toContain("string block");
|
|
849
|
+
expect(prompt).toContain("text block");
|
|
850
|
+
expect(prompt).toContain("result content");
|
|
851
|
+
expect(prompt).toContain('[tool_use:bash] {"command":"ls"}');
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe("buildLlamafileArgs, buildAgentfileArgs and binary drivers", () => {
|
|
856
|
+
it("tests getBuiltinTools and binary drivers buildArgs / buildCliArgs", async () => {
|
|
857
|
+
const { getBuiltinTools, getBinaryDriver, buildAgentfileArgs } =
|
|
858
|
+
await import("./llamafile-manager.js");
|
|
859
|
+
|
|
860
|
+
expect(getBuiltinTools("llamafile")).toBeDefined();
|
|
861
|
+
expect(getBuiltinTools("agentfile")).toBeDefined();
|
|
862
|
+
|
|
863
|
+
const llamaDriver = getBinaryDriver("llamafile");
|
|
864
|
+
expect(llamaDriver).toBeDefined();
|
|
865
|
+
const llamaArgs = llamaDriver!.buildArgs!({
|
|
866
|
+
prompt: "hello llama",
|
|
867
|
+
nPredict: 256,
|
|
868
|
+
ctxSize: 4096,
|
|
869
|
+
offline: true,
|
|
870
|
+
});
|
|
871
|
+
expect(llamaArgs).toContain("--offline");
|
|
872
|
+
expect(llamaArgs).toContain("hello llama");
|
|
873
|
+
|
|
874
|
+
const llamaCliArgs = llamaDriver!.buildCliArgs!({
|
|
875
|
+
prompt: "cli hello",
|
|
876
|
+
});
|
|
877
|
+
expect(llamaCliArgs).toContain("cli hello");
|
|
878
|
+
|
|
879
|
+
const agentDriver = getBinaryDriver("agentfile");
|
|
880
|
+
expect(agentDriver).toBeDefined();
|
|
881
|
+
const agentArgs = agentDriver!.buildArgs!({
|
|
882
|
+
prompt: "hello agent",
|
|
883
|
+
tools: ["bash"],
|
|
884
|
+
modelPath: "/path/to/model",
|
|
885
|
+
runtimeOptions: {
|
|
886
|
+
systemPrompt: "system text",
|
|
887
|
+
confirm: true,
|
|
888
|
+
toolsRuntime: "node",
|
|
889
|
+
searxngUrl: "http://searxng.local",
|
|
890
|
+
sessionFile: "sess.json",
|
|
891
|
+
traceFile: "trace.json",
|
|
892
|
+
maxIterations: 5,
|
|
893
|
+
think: true,
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
expect(agentArgs).toContain("-m");
|
|
897
|
+
expect(agentArgs).toContain("/path/to/model");
|
|
898
|
+
expect(agentArgs).toContain("-s");
|
|
899
|
+
expect(agentArgs).toContain("system text");
|
|
900
|
+
expect(agentArgs).toContain("--tools");
|
|
901
|
+
expect(agentArgs).toContain("bash");
|
|
902
|
+
expect(agentArgs).toContain("--confirm");
|
|
903
|
+
expect(agentArgs).toContain("--tools-runtime");
|
|
904
|
+
expect(agentArgs).toContain("--searxng-url");
|
|
905
|
+
expect(agentArgs).toContain("--session");
|
|
906
|
+
expect(agentArgs).toContain("--trace");
|
|
907
|
+
expect(agentArgs).toContain("--max-iterations");
|
|
908
|
+
expect(agentArgs).toContain("--think");
|
|
909
|
+
|
|
910
|
+
const agentCliArgs = agentDriver!.buildCliArgs!({
|
|
911
|
+
prompt: "agent cli",
|
|
912
|
+
tools: [],
|
|
913
|
+
runtimeOptions: {
|
|
914
|
+
confirm: false,
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
expect(agentCliArgs).toContain("--tools");
|
|
918
|
+
expect(agentCliArgs).toContain("none");
|
|
919
|
+
expect(agentCliArgs).toContain("--yes");
|
|
920
|
+
|
|
921
|
+
const toolsArrayArgs = buildAgentfileArgs({
|
|
922
|
+
prompt: "tool array",
|
|
923
|
+
tools: [{ name: "tool_a" }, { function: { name: "tool_b" } }],
|
|
924
|
+
});
|
|
925
|
+
expect(toolsArrayArgs).toContain("tool_a,tool_b");
|
|
926
|
+
|
|
927
|
+
const toolsFromRuntime = buildAgentfileArgs({
|
|
928
|
+
prompt: "runtime tools",
|
|
929
|
+
runtimeOptions: { tools: "custom_runtime_tools" },
|
|
930
|
+
});
|
|
931
|
+
expect(toolsFromRuntime).toContain("custom_runtime_tools");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it("executes get_datetime in CLI mode and accesses active processes", async () => {
|
|
935
|
+
const service = await getService();
|
|
936
|
+
const dt = await service.executeTool("get_datetime", {}, { mode: "cli" });
|
|
937
|
+
expect(dt).toHaveProperty("datetime");
|
|
938
|
+
expect(service.__getActiveProcesses()).toBeDefined();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("filters transport headers in invokeLlamafileServer", async () => {
|
|
942
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
943
|
+
|
|
944
|
+
const req: any = { headers: {} };
|
|
945
|
+
const res: any = {
|
|
946
|
+
status: jest.fn().mockReturnThis(),
|
|
947
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
948
|
+
send: jest.fn().mockReturnThis(),
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const upstreamHeaders = new Headers({
|
|
952
|
+
"Content-Encoding": "gzip",
|
|
953
|
+
"Transfer-Encoding": "chunked",
|
|
954
|
+
"Content-Length": "1234",
|
|
955
|
+
Connection: "close",
|
|
956
|
+
"X-Custom-Header": "custom-val",
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValue({
|
|
960
|
+
ok: true,
|
|
961
|
+
status: 200,
|
|
962
|
+
headers: upstreamHeaders,
|
|
963
|
+
arrayBuffer: async () => Buffer.from("response"),
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
await invokeLlamafileServer(
|
|
967
|
+
req,
|
|
968
|
+
res,
|
|
969
|
+
{ model: "custom-model", messages: [] },
|
|
970
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
971
|
+
false,
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
975
|
+
"x-custom-header",
|
|
976
|
+
"custom-val",
|
|
977
|
+
);
|
|
978
|
+
expect(res.setHeader).not.toHaveBeenCalledWith(
|
|
979
|
+
"content-encoding",
|
|
980
|
+
expect.anything(),
|
|
981
|
+
);
|
|
982
|
+
expect(res.setHeader).not.toHaveBeenCalledWith(
|
|
983
|
+
"content-length",
|
|
984
|
+
expect.anything(),
|
|
985
|
+
);
|
|
986
|
+
});
|
|
632
987
|
});
|
|
633
988
|
|
|
634
989
|
describe("invokeCli multi-tool calls and stream filtering", () => {
|
|
@@ -1080,5 +1435,318 @@ describe("LlamafileManagerService", () => {
|
|
|
1080
1435
|
);
|
|
1081
1436
|
expect(res.body.choices[0].finish_reason).toBe("length");
|
|
1082
1437
|
});
|
|
1438
|
+
|
|
1439
|
+
it("handles invokeCli streaming with tool calls", async () => {
|
|
1440
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
1441
|
+
readdirMock.mockResolvedValue([
|
|
1442
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
1443
|
+
]);
|
|
1444
|
+
accessMock.mockResolvedValue(undefined);
|
|
1445
|
+
|
|
1446
|
+
const service = await getService();
|
|
1447
|
+
|
|
1448
|
+
const fakeChild: any = new EventEmitter();
|
|
1449
|
+
fakeChild.pid = 1234;
|
|
1450
|
+
fakeChild.stdout = new EventEmitter();
|
|
1451
|
+
fakeChild.stderr = new EventEmitter();
|
|
1452
|
+
fakeChild.exitCode = null;
|
|
1453
|
+
fakeChild.signalCode = null;
|
|
1454
|
+
fakeChild.killed = false;
|
|
1455
|
+
|
|
1456
|
+
spawnMock.mockImplementation(() => {
|
|
1457
|
+
queueMicrotask(() => fakeChild.emit("spawn"));
|
|
1458
|
+
return fakeChild;
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
const mockReq: any = new EventEmitter();
|
|
1462
|
+
mockReq.headers = {};
|
|
1463
|
+
mockReq.socket = new EventEmitter();
|
|
1464
|
+
|
|
1465
|
+
const writtenChunks: string[] = [];
|
|
1466
|
+
const res = Object.assign(new EventEmitter(), {
|
|
1467
|
+
statusCode: 200,
|
|
1468
|
+
headersSent: false,
|
|
1469
|
+
status: jest.fn().mockReturnThis(),
|
|
1470
|
+
write: jest.fn((chunk: string) => {
|
|
1471
|
+
writtenChunks.push(chunk);
|
|
1472
|
+
res.headersSent = true;
|
|
1473
|
+
}),
|
|
1474
|
+
end: jest.fn(),
|
|
1475
|
+
setHeader: jest.fn(),
|
|
1476
|
+
flushHeaders: jest.fn(),
|
|
1477
|
+
}) as any;
|
|
1478
|
+
|
|
1479
|
+
const invokePromise = service.invokeCli(
|
|
1480
|
+
mockReq,
|
|
1481
|
+
res,
|
|
1482
|
+
{
|
|
1483
|
+
model: "model1",
|
|
1484
|
+
messages: [{ role: "user", content: "call tool" }],
|
|
1485
|
+
stream: true,
|
|
1486
|
+
},
|
|
1487
|
+
{ model: "model1", offline: false },
|
|
1488
|
+
false,
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
setImmediate(() => {
|
|
1492
|
+
fakeChild.stdout.emit(
|
|
1493
|
+
"data",
|
|
1494
|
+
Buffer.from('call:read_file{path:"test.txt"}'),
|
|
1495
|
+
);
|
|
1496
|
+
fakeChild.emit("close", 0);
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
await invokePromise;
|
|
1500
|
+
|
|
1501
|
+
expect(res.write).toHaveBeenCalled();
|
|
1502
|
+
expect(res.end).toHaveBeenCalled();
|
|
1503
|
+
const fullWritten = writtenChunks.join("");
|
|
1504
|
+
expect(fullWritten).toContain("read_file");
|
|
1505
|
+
expect(fullWritten).toContain('"finish_reason":"tool_calls"');
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("handles invokeCli streaming process failure on close", async () => {
|
|
1509
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
1510
|
+
readdirMock.mockResolvedValue([
|
|
1511
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
1512
|
+
]);
|
|
1513
|
+
accessMock.mockResolvedValue(undefined);
|
|
1514
|
+
|
|
1515
|
+
const service = await getService();
|
|
1516
|
+
|
|
1517
|
+
const fakeChild: any = new EventEmitter();
|
|
1518
|
+
fakeChild.pid = 1234;
|
|
1519
|
+
fakeChild.stdout = new EventEmitter();
|
|
1520
|
+
fakeChild.stderr = new EventEmitter();
|
|
1521
|
+
fakeChild.exitCode = null;
|
|
1522
|
+
fakeChild.signalCode = null;
|
|
1523
|
+
fakeChild.killed = false;
|
|
1524
|
+
|
|
1525
|
+
spawnMock.mockImplementation(() => {
|
|
1526
|
+
queueMicrotask(() => fakeChild.emit("spawn"));
|
|
1527
|
+
return fakeChild;
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
const mockReq: any = new EventEmitter();
|
|
1531
|
+
mockReq.headers = {};
|
|
1532
|
+
mockReq.socket = new EventEmitter();
|
|
1533
|
+
|
|
1534
|
+
const writtenChunks: string[] = [];
|
|
1535
|
+
const res = Object.assign(new EventEmitter(), {
|
|
1536
|
+
statusCode: 200,
|
|
1537
|
+
headersSent: false,
|
|
1538
|
+
status: jest.fn().mockReturnThis(),
|
|
1539
|
+
write: jest.fn((chunk: string) => {
|
|
1540
|
+
writtenChunks.push(chunk);
|
|
1541
|
+
res.headersSent = true;
|
|
1542
|
+
}),
|
|
1543
|
+
end: jest.fn(),
|
|
1544
|
+
setHeader: jest.fn(),
|
|
1545
|
+
flushHeaders: jest.fn(),
|
|
1546
|
+
}) as any;
|
|
1547
|
+
|
|
1548
|
+
const invokePromise = service.invokeCli(
|
|
1549
|
+
mockReq,
|
|
1550
|
+
res,
|
|
1551
|
+
{
|
|
1552
|
+
model: "model1",
|
|
1553
|
+
messages: [{ role: "user", content: "crash" }],
|
|
1554
|
+
stream: true,
|
|
1555
|
+
},
|
|
1556
|
+
{ model: "model1", offline: false },
|
|
1557
|
+
false,
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
setImmediate(() => {
|
|
1561
|
+
fakeChild.stderr.emit("data", Buffer.from("Fatal llamafile segfault"));
|
|
1562
|
+
fakeChild.emit("close", 1);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
await invokePromise;
|
|
1566
|
+
|
|
1567
|
+
expect(res.write).toHaveBeenCalled();
|
|
1568
|
+
expect(res.end).toHaveBeenCalled();
|
|
1569
|
+
const fullWritten = writtenChunks.join("");
|
|
1570
|
+
expect(fullWritten).toContain("Fatal llamafile segfault");
|
|
1571
|
+
expect(fullWritten).toContain("[DONE]");
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it("handles invokeCli non-streaming failure on non-zero exit code", async () => {
|
|
1575
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
1576
|
+
readdirMock.mockResolvedValue([
|
|
1577
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
1578
|
+
]);
|
|
1579
|
+
accessMock.mockResolvedValue(undefined);
|
|
1580
|
+
|
|
1581
|
+
const service = await getService();
|
|
1582
|
+
|
|
1583
|
+
const fakeChild: any = new EventEmitter();
|
|
1584
|
+
fakeChild.pid = 1234;
|
|
1585
|
+
fakeChild.stdout = new EventEmitter();
|
|
1586
|
+
fakeChild.stderr = new EventEmitter();
|
|
1587
|
+
fakeChild.exitCode = null;
|
|
1588
|
+
fakeChild.signalCode = null;
|
|
1589
|
+
fakeChild.killed = false;
|
|
1590
|
+
|
|
1591
|
+
spawnMock.mockImplementation(() => {
|
|
1592
|
+
queueMicrotask(() => fakeChild.emit("spawn"));
|
|
1593
|
+
return fakeChild;
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
const mockReq: any = new EventEmitter();
|
|
1597
|
+
mockReq.headers = {};
|
|
1598
|
+
mockReq.socket = new EventEmitter();
|
|
1599
|
+
|
|
1600
|
+
const res = Object.assign(new EventEmitter(), {
|
|
1601
|
+
statusCode: 200,
|
|
1602
|
+
status: jest.fn().mockReturnThis(),
|
|
1603
|
+
json: jest.fn(),
|
|
1604
|
+
}) as any;
|
|
1605
|
+
|
|
1606
|
+
const invokePromise = service.invokeCli(
|
|
1607
|
+
mockReq,
|
|
1608
|
+
res,
|
|
1609
|
+
{
|
|
1610
|
+
model: "model1",
|
|
1611
|
+
messages: [{ role: "user", content: "fail" }],
|
|
1612
|
+
},
|
|
1613
|
+
{ model: "model1", offline: false },
|
|
1614
|
+
false,
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
setImmediate(() => {
|
|
1618
|
+
fakeChild.stderr.emit("data", Buffer.from("Out of memory"));
|
|
1619
|
+
fakeChild.emit("close", 1);
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
await invokePromise;
|
|
1623
|
+
|
|
1624
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
1625
|
+
expect(res.json).toHaveBeenCalledWith({ error: "Out of memory" });
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it("handles invokeCli child process error", async () => {
|
|
1629
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
1630
|
+
readdirMock.mockResolvedValue([
|
|
1631
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
1632
|
+
]);
|
|
1633
|
+
accessMock.mockResolvedValue(undefined);
|
|
1634
|
+
|
|
1635
|
+
const service = await getService();
|
|
1636
|
+
|
|
1637
|
+
const fakeChild: any = new EventEmitter();
|
|
1638
|
+
fakeChild.pid = 1234;
|
|
1639
|
+
fakeChild.stdout = new EventEmitter();
|
|
1640
|
+
fakeChild.stderr = new EventEmitter();
|
|
1641
|
+
fakeChild.exitCode = null;
|
|
1642
|
+
fakeChild.signalCode = null;
|
|
1643
|
+
fakeChild.killed = false;
|
|
1644
|
+
|
|
1645
|
+
spawnMock.mockImplementation(() => {
|
|
1646
|
+
queueMicrotask(() => fakeChild.emit("spawn"));
|
|
1647
|
+
return fakeChild;
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
const mockReq: any = new EventEmitter();
|
|
1651
|
+
mockReq.headers = {};
|
|
1652
|
+
mockReq.socket = new EventEmitter();
|
|
1653
|
+
|
|
1654
|
+
const res = Object.assign(new EventEmitter(), {
|
|
1655
|
+
statusCode: 200,
|
|
1656
|
+
headersSent: false,
|
|
1657
|
+
status: jest.fn().mockReturnThis(),
|
|
1658
|
+
json: jest.fn(),
|
|
1659
|
+
}) as any;
|
|
1660
|
+
|
|
1661
|
+
const invokePromise = service.invokeCli(
|
|
1662
|
+
mockReq,
|
|
1663
|
+
res,
|
|
1664
|
+
{
|
|
1665
|
+
model: "model1",
|
|
1666
|
+
messages: [{ role: "user", content: "spawn fail" }],
|
|
1667
|
+
},
|
|
1668
|
+
{ model: "model1", offline: false },
|
|
1669
|
+
false,
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
setImmediate(() => {
|
|
1673
|
+
fakeChild.emit("error", new Error("EACCES permission denied"));
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
await invokePromise;
|
|
1677
|
+
|
|
1678
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
1679
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
1680
|
+
expect.objectContaining({
|
|
1681
|
+
error: expect.stringContaining("EACCES permission denied"),
|
|
1682
|
+
}),
|
|
1683
|
+
);
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
it("probes --offline support flag when offline option is true", async () => {
|
|
1687
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
1688
|
+
readdirMock.mockResolvedValue([
|
|
1689
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
1690
|
+
]);
|
|
1691
|
+
accessMock.mockResolvedValue(undefined);
|
|
1692
|
+
|
|
1693
|
+
const service = await getService();
|
|
1694
|
+
|
|
1695
|
+
let callCount = 0;
|
|
1696
|
+
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
|
1697
|
+
const child: any = new EventEmitter();
|
|
1698
|
+
child.pid = 5000 + callCount++;
|
|
1699
|
+
child.stdout = new EventEmitter();
|
|
1700
|
+
child.stderr = new EventEmitter();
|
|
1701
|
+
child.exitCode = null;
|
|
1702
|
+
child.signalCode = null;
|
|
1703
|
+
child.killed = false;
|
|
1704
|
+
|
|
1705
|
+
queueMicrotask(() => {
|
|
1706
|
+
child.emit("spawn");
|
|
1707
|
+
setImmediate(() => {
|
|
1708
|
+
if (args && args.includes("--help")) {
|
|
1709
|
+
child.stdout.emit(
|
|
1710
|
+
"data",
|
|
1711
|
+
Buffer.from("Options:\n --offline Do not connect\n"),
|
|
1712
|
+
);
|
|
1713
|
+
child.emit("close", 0);
|
|
1714
|
+
} else {
|
|
1715
|
+
child.stdout.emit("data", Buffer.from("Answer offline"));
|
|
1716
|
+
child.emit("close", 0);
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
return child;
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
const mockReq: any = new EventEmitter();
|
|
1725
|
+
mockReq.headers = {};
|
|
1726
|
+
mockReq.socket = new EventEmitter();
|
|
1727
|
+
|
|
1728
|
+
const res = Object.assign(new EventEmitter(), {
|
|
1729
|
+
statusCode: 200,
|
|
1730
|
+
body: undefined as any,
|
|
1731
|
+
json: jest.fn((val: any) => {
|
|
1732
|
+
res.body = val;
|
|
1733
|
+
}),
|
|
1734
|
+
status: jest.fn().mockReturnThis(),
|
|
1735
|
+
}) as any;
|
|
1736
|
+
|
|
1737
|
+
await service.invokeCli(
|
|
1738
|
+
mockReq,
|
|
1739
|
+
res,
|
|
1740
|
+
{
|
|
1741
|
+
model: "model1",
|
|
1742
|
+
messages: [{ role: "user", content: "hello" }],
|
|
1743
|
+
},
|
|
1744
|
+
{ model: "model1", offline: true },
|
|
1745
|
+
true,
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1748
|
+
expect(res.json).toHaveBeenCalled();
|
|
1749
|
+
expect(service.__getOfflineSupportCache().size).toBeGreaterThan(0);
|
|
1750
|
+
});
|
|
1083
1751
|
});
|
|
1084
1752
|
});
|