shadow-claw 1.40.0 → 1.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.well-known/mcp/server-card.json +2 -2
- package/.well-known/mcp/servers.json +1 -1
- package/.well-known/mcp.json +2 -2
- package/dist/cli/cli.js +2 -2
- package/dist/lib/components/index.js +2 -2
- package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
- package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
- package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
- package/dist/lib/index.js +2 -2
- package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
- package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
- package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
- package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
- package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
- package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
- package/dist/public/.well-known/mcp/server-card.json +2 -2
- package/dist/public/.well-known/mcp/servers.json +1 -1
- package/dist/public/.well-known/mcp.json +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/publishing/index.html +2 -2
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +2 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
- package/src/core/effect.test.ts +19 -0
- package/src/core/orchestrator/orchestrator-state.ts +2 -1
- package/src/core/orchestrator/orchestrator.test.ts +686 -5
- package/src/core/orchestrator/orchestrator.ts +3 -10
- package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
- package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
- package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
- package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
- package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
- package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
- package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
- package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
- package/src/core/orchestrator/utils/initTasks.ts +5 -4
- package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
- package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
- package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
- package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
- package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
- package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
- package/src/core/orchestrator/utils/operations/task.ts +1 -1
- package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
- package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
- package/src/core/orchestrator/utils/settings.test.ts +248 -81
- package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
- package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
- package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
- package/src/server/routes/mcp.ts +2 -2
- package/src/stores/file-viewer.test.ts +11 -0
- package/src/stores/theme.test.ts +41 -2
- package/src/stores/toast.test.ts +25 -0
- package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
- package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
- package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
- package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
- package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
- package/src/worker/tools/node-transformers-executor.test.ts +105 -6
- package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
- package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
- package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
- package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
- package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
- package/src/worker/tools/tasks/update-task.test.ts +78 -14
- package/src/worker/tools/ui/javascript.test.ts +70 -17
- package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
- package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
package/src/stores/theme.test.ts
CHANGED
|
@@ -11,8 +11,8 @@ describe("themeStore", () => {
|
|
|
11
11
|
localStorage.clear();
|
|
12
12
|
document.documentElement.className = "";
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
(window
|
|
14
|
+
// Spy on dispatchEvent so event listeners still fire
|
|
15
|
+
jest.spyOn(window, "dispatchEvent");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("applies and persists explicit theme", async () => {
|
|
@@ -135,4 +135,43 @@ describe("themeStore", () => {
|
|
|
135
135
|
expect(themeStore.resolved).toBe("light");
|
|
136
136
|
expect(localStorage.getItem("shadow-claw-theme")).toBe("system");
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it("handles storage event for tab synchronization", async () => {
|
|
140
|
+
const { getNamespacedStorageKey } =
|
|
141
|
+
await import("../utils/namespacedStorage.js");
|
|
142
|
+
const { themeStore } = await import("./theme.js");
|
|
143
|
+
const targetKey = getNamespacedStorageKey("shadow-claw-theme");
|
|
144
|
+
|
|
145
|
+
// Other key should be ignored
|
|
146
|
+
const otherEv = new Event("storage") as any;
|
|
147
|
+
otherEv.key = "other-key";
|
|
148
|
+
otherEv.newValue = "dark";
|
|
149
|
+
window.dispatchEvent(otherEv);
|
|
150
|
+
expect(themeStore.theme).not.toBe("dark");
|
|
151
|
+
|
|
152
|
+
// Target key with null newValue should be ignored
|
|
153
|
+
const nullEv = new Event("storage") as any;
|
|
154
|
+
nullEv.key = targetKey;
|
|
155
|
+
nullEv.newValue = null;
|
|
156
|
+
window.dispatchEvent(nullEv);
|
|
157
|
+
|
|
158
|
+
// Target key with valid newValue updates theme
|
|
159
|
+
const validEv = new Event("storage") as any;
|
|
160
|
+
validEv.key = targetKey;
|
|
161
|
+
validEv.newValue = "dark";
|
|
162
|
+
window.dispatchEvent(validEv);
|
|
163
|
+
|
|
164
|
+
expect(themeStore.theme).toBe("dark");
|
|
165
|
+
expect(themeStore.resolved).toBe("dark");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("falls back gracefully when matchMedia is not a function", async () => {
|
|
169
|
+
(window as any).matchMedia = undefined;
|
|
170
|
+
jest.resetModules();
|
|
171
|
+
localStorage.clear();
|
|
172
|
+
const { themeStore } = await import("./theme.js");
|
|
173
|
+
|
|
174
|
+
themeStore.setTheme("system");
|
|
175
|
+
expect(themeStore.resolved).toBe("light");
|
|
176
|
+
});
|
|
138
177
|
});
|
package/src/stores/toast.test.ts
CHANGED
|
@@ -101,6 +101,31 @@ describe("ToastStore", () => {
|
|
|
101
101
|
|
|
102
102
|
// No-op for non-existent or action-less toast
|
|
103
103
|
await store.runAction(999);
|
|
104
|
+
|
|
105
|
+
const noActionId = store.show("without action");
|
|
106
|
+
await store.runAction(noActionId);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("pause and resume handle missing or invalid timers gracefully", () => {
|
|
110
|
+
const store = new ToastStore();
|
|
111
|
+
|
|
112
|
+
// Calling pause on nonexistent toast or already paused toast
|
|
113
|
+
expect(() => store.pause(999)).not.toThrow();
|
|
114
|
+
|
|
115
|
+
const id = store.show("toast", { duration: 1000 });
|
|
116
|
+
store.pause(id);
|
|
117
|
+
// Pause again when timeoutId is undefined
|
|
118
|
+
expect(() => store.pause(id)).not.toThrow();
|
|
119
|
+
|
|
120
|
+
// Calling resume on nonexistent toast, or already running toast, or expired toast
|
|
121
|
+
expect(() => store.resume(999)).not.toThrow();
|
|
122
|
+
|
|
123
|
+
const runningId = store.show("running", { duration: 1000 });
|
|
124
|
+
// Resume when timer.timeoutId is still active
|
|
125
|
+
expect(() => store.resume(runningId)).not.toThrow();
|
|
126
|
+
|
|
127
|
+
// Clear timer on nonexistent toast
|
|
128
|
+
expect(() => store.clearTimer(999)).not.toThrow();
|
|
104
129
|
});
|
|
105
130
|
|
|
106
131
|
test("resolvers handle invalid inputs gracefully", () => {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { describe, it, expect } from "@jest/globals";
|
|
1
|
+
import { describe, it, expect, afterEach } from "@jest/globals";
|
|
2
2
|
import { nativeBashExecutor } from "./native-bash-executor.js";
|
|
3
3
|
|
|
4
4
|
describe("nativeBashExecutor", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
delete (globalThis as Record<string, unknown>).__getStorageRootPath;
|
|
7
|
+
});
|
|
8
|
+
|
|
5
9
|
it("executes a shell command and captures stdout", async () => {
|
|
6
10
|
const output = await nativeBashExecutor({
|
|
7
11
|
command: "echo 'hello from native bash'",
|
|
@@ -26,4 +30,30 @@ describe("nativeBashExecutor", () => {
|
|
|
26
30
|
});
|
|
27
31
|
expect(output).toContain("test stderr output");
|
|
28
32
|
});
|
|
33
|
+
|
|
34
|
+
it("captures both stdout and stderr formatted with newline", async () => {
|
|
35
|
+
const output = await nativeBashExecutor({
|
|
36
|
+
command: 'node -e \'process.stdout.write("out"); console.error("err");\'',
|
|
37
|
+
timeoutSec: 5,
|
|
38
|
+
});
|
|
39
|
+
expect(output).toContain("out\nerr");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("falls back to error message when command fails with empty output", async () => {
|
|
43
|
+
const output = await nativeBashExecutor({
|
|
44
|
+
command: "node -e 'process.exit(42)'",
|
|
45
|
+
timeoutSec: 5,
|
|
46
|
+
});
|
|
47
|
+
expect(output).toContain("Command failed");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses __getStorageRootPath when provided on globalThis", async () => {
|
|
51
|
+
(globalThis as Record<string, unknown>).__getStorageRootPath = () =>
|
|
52
|
+
process.cwd();
|
|
53
|
+
const output = await nativeBashExecutor({
|
|
54
|
+
command: "pwd",
|
|
55
|
+
timeoutSec: 5,
|
|
56
|
+
});
|
|
57
|
+
expect(output.trim()).toBe(process.cwd());
|
|
58
|
+
});
|
|
29
59
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { extractConflictPaths } from "./extractConflictPaths.js";
|
|
3
|
+
|
|
4
|
+
describe("extractConflictPaths", () => {
|
|
5
|
+
it("extracts multiple file paths from conflict message", () => {
|
|
6
|
+
const message =
|
|
7
|
+
"Automatic merge failed; conflicts in the following files: fileA.ts, fileB.ts, fileC.ts";
|
|
8
|
+
expect(extractConflictPaths(message)).toEqual([
|
|
9
|
+
"fileA.ts",
|
|
10
|
+
"fileB.ts",
|
|
11
|
+
"fileC.ts",
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("extracts single file path from singular conflict message", () => {
|
|
16
|
+
const message = "conflict in the following file: readme.md";
|
|
17
|
+
expect(extractConflictPaths(message)).toEqual(["readme.md"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns empty array when message does not match conflict format", () => {
|
|
21
|
+
const message = "Merge successful with fast-forward";
|
|
22
|
+
expect(extractConflictPaths(message)).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { parseConflictRegions } from "./parseConflictRegions.js";
|
|
3
|
+
|
|
4
|
+
describe("parseConflictRegions", () => {
|
|
5
|
+
it("parses single standard conflict region", () => {
|
|
6
|
+
const content = [
|
|
7
|
+
"before",
|
|
8
|
+
"<<<<<<< HEAD",
|
|
9
|
+
"our content line 1",
|
|
10
|
+
"our content line 2",
|
|
11
|
+
"=======",
|
|
12
|
+
"their content",
|
|
13
|
+
">>>>>>> branch-feat",
|
|
14
|
+
"after",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
const regions = parseConflictRegions(content);
|
|
18
|
+
expect(regions).toHaveLength(1);
|
|
19
|
+
expect(regions[0]).toEqual({
|
|
20
|
+
ours: "our content line 1\nour content line 2",
|
|
21
|
+
oursLabel: "HEAD",
|
|
22
|
+
startLine: 2,
|
|
23
|
+
theirs: "their content",
|
|
24
|
+
theirsLabel: "branch-feat",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("handles unclosed conflict regions that reach end-of-file without closing marker", () => {
|
|
29
|
+
const content = [
|
|
30
|
+
"<<<<<<< HEAD",
|
|
31
|
+
"our changes",
|
|
32
|
+
"=======",
|
|
33
|
+
"their unclosed changes",
|
|
34
|
+
].join("\n");
|
|
35
|
+
|
|
36
|
+
const regions = parseConflictRegions(content);
|
|
37
|
+
expect(regions).toHaveLength(1);
|
|
38
|
+
expect(regions[0].theirsLabel).toBe("");
|
|
39
|
+
expect(regions[0].theirs).toBe("their unclosed changes");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns empty array when no conflict markers are present", () => {
|
|
43
|
+
const content = "regular file content\nwithout conflicts";
|
|
44
|
+
expect(parseConflictRegions(content)).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "@jest/globals";
|
|
2
2
|
import { resolveCorsProxy } from "./resolveCorsProxy.js";
|
|
3
3
|
import { setHeadlessMode } from "../../../../config/headless.js";
|
|
4
|
+
import type { ShadowClawDatabase } from "../../../../db/types.js";
|
|
5
|
+
import type { GitToolDeps } from "../../../../subsystems/git/types.js";
|
|
4
6
|
|
|
5
7
|
describe("resolveCorsProxy", () => {
|
|
6
|
-
|
|
7
|
-
let mockDeps:
|
|
8
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
9
|
+
let mockDeps: GitToolDeps;
|
|
8
10
|
let configStore: Record<string, string>;
|
|
9
11
|
|
|
10
12
|
beforeEach(() => {
|
|
11
13
|
configStore = {};
|
|
12
|
-
mockDb = {};
|
|
13
14
|
mockDeps = {
|
|
14
15
|
configKeys: {
|
|
15
16
|
GIT_CORS_PROXY: "git_cors_proxy",
|
|
16
17
|
GIT_PROXY_URL: "git_proxy_url",
|
|
17
18
|
},
|
|
18
|
-
getConfig: async (_db:
|
|
19
|
+
getConfig: async (_db: ShadowClawDatabase, key: string) =>
|
|
20
|
+
configStore[key],
|
|
19
21
|
getProxyUrl: (pref: string, custom?: string) => {
|
|
20
22
|
if (pref === "custom") return custom || "https://custom.proxy";
|
|
21
23
|
if (pref === "public") return "https://public.proxy";
|
|
22
24
|
return "http://127.0.0.1:8888/git-proxy";
|
|
23
25
|
},
|
|
24
|
-
};
|
|
26
|
+
} as unknown as GitToolDeps;
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("returns local proxy URL in browser mode by default", async () => {
|
|
@@ -44,10 +46,29 @@ describe("resolveCorsProxy", () => {
|
|
|
44
46
|
expect(result).toBe("https://my-proxy.internal");
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
it("falls back when custom proxy is selected but no custom URL is provided", async () => {
|
|
50
|
+
setHeadlessMode(false);
|
|
51
|
+
configStore["git_cors_proxy"] = "custom";
|
|
52
|
+
configStore["git_proxy_url"] = "";
|
|
53
|
+
const result = await resolveCorsProxy(mockDb, mockDeps);
|
|
54
|
+
expect(result).toBe("http://127.0.0.1:8888/git-proxy");
|
|
55
|
+
});
|
|
56
|
+
|
|
47
57
|
it("returns public proxy URL when configured in browser mode", async () => {
|
|
48
58
|
setHeadlessMode(false);
|
|
49
59
|
configStore["git_cors_proxy"] = "public";
|
|
50
60
|
const result = await resolveCorsProxy(mockDb, mockDeps);
|
|
51
61
|
expect(result).toBe("https://public.proxy");
|
|
52
62
|
});
|
|
63
|
+
|
|
64
|
+
it("returns undefined when pref is direct or none", async () => {
|
|
65
|
+
setHeadlessMode(false);
|
|
66
|
+
configStore["git_cors_proxy"] = "direct";
|
|
67
|
+
const directResult = await resolveCorsProxy(mockDb, mockDeps);
|
|
68
|
+
expect(directResult).toBeUndefined();
|
|
69
|
+
|
|
70
|
+
configStore["git_cors_proxy"] = "none";
|
|
71
|
+
const noneResult = await resolveCorsProxy(mockDb, mockDeps);
|
|
72
|
+
expect(noneResult).toBeUndefined();
|
|
73
|
+
});
|
|
53
74
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { truncateSnippet } from "./truncateSnippet.js";
|
|
3
|
+
|
|
4
|
+
describe("truncateSnippet", () => {
|
|
5
|
+
it("returns unchanged text when line count is less than or equal to maxLines", () => {
|
|
6
|
+
const text = "line 1\nline 2\nline 3";
|
|
7
|
+
expect(truncateSnippet(text, 5)).toBe(text);
|
|
8
|
+
expect(truncateSnippet(text, 3)).toBe(text);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("truncates text and appends remaining line count when line count exceeds maxLines", () => {
|
|
12
|
+
const text = "line 1\nline 2\nline 3\nline 4\nline 5";
|
|
13
|
+
const result = truncateSnippet(text, 2);
|
|
14
|
+
expect(result).toBe("line 1\nline 2\n [... 3 more lines]");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
/** @jest-environment node */
|
|
2
2
|
import { describe, it, expect, jest, beforeEach } from "@jest/globals";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import type { TransformersRuntimeService } from "../../server/services/transformers-runtime.js";
|
|
4
|
+
|
|
5
|
+
const mockCreateServiceInstance: TransformersRuntimeService = {
|
|
6
|
+
runChatCompletion: jest.fn(async () => ({
|
|
7
|
+
text: "Created service default output",
|
|
8
|
+
promptTokens: 5,
|
|
9
|
+
completionTokens: 5,
|
|
10
|
+
})),
|
|
11
|
+
} as unknown as TransformersRuntimeService;
|
|
12
|
+
|
|
13
|
+
const mockCreateTransformersRuntimeService = jest.fn(
|
|
14
|
+
() => mockCreateServiceInstance,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
jest.unstable_mockModule(
|
|
18
|
+
"../../server/services/transformers-runtime.js",
|
|
19
|
+
() => ({
|
|
20
|
+
createTransformersRuntimeService: mockCreateTransformersRuntimeService,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const { executeNodeTransformersCompletion, setTransformersServiceForTests } =
|
|
25
|
+
await import("./node-transformers-executor.js");
|
|
7
26
|
|
|
8
27
|
describe("executeNodeTransformersCompletion", () => {
|
|
9
|
-
let mockService:
|
|
28
|
+
let mockService: TransformersRuntimeService;
|
|
10
29
|
|
|
11
30
|
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
12
32
|
mockService = {
|
|
13
33
|
runChatCompletion: jest.fn(async () => ({
|
|
14
34
|
text: "Hello from local ONNX!",
|
|
15
35
|
promptTokens: 12,
|
|
16
36
|
completionTokens: 8,
|
|
17
37
|
})),
|
|
18
|
-
};
|
|
38
|
+
} as unknown as TransformersRuntimeService;
|
|
19
39
|
setTransformersServiceForTests(mockService);
|
|
20
40
|
});
|
|
21
41
|
|
|
@@ -55,4 +75,83 @@ describe("executeNodeTransformersCompletion", () => {
|
|
|
55
75
|
}),
|
|
56
76
|
);
|
|
57
77
|
});
|
|
78
|
+
|
|
79
|
+
it("handles tool calls returned by the model output", async () => {
|
|
80
|
+
mockService.runChatCompletion = jest.fn(async () => ({
|
|
81
|
+
text: 'call:read_file{"path":"test.txt"}',
|
|
82
|
+
promptTokens: 20,
|
|
83
|
+
completionTokens: 15,
|
|
84
|
+
})) as typeof mockService.runChatCompletion;
|
|
85
|
+
|
|
86
|
+
const tools = [
|
|
87
|
+
{
|
|
88
|
+
name: "read_file",
|
|
89
|
+
description: "Read a file",
|
|
90
|
+
parameters: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: { path: { type: "string" } },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const response = await executeNodeTransformersCompletion({
|
|
98
|
+
modelId: "onnx-community/gemma-4-E2B-it-ONNX",
|
|
99
|
+
messages: [{ role: "user", content: "Read test.txt" }],
|
|
100
|
+
tools,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(mockService.runChatCompletion).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
tools: expect.arrayContaining([
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
type: "function",
|
|
108
|
+
function: expect.objectContaining({ name: "read_file" }),
|
|
109
|
+
}),
|
|
110
|
+
]),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(response.choices[0].finish_reason).toBe("tool_calls");
|
|
115
|
+
expect(response.choices[0].message.content).toBeNull();
|
|
116
|
+
expect(response.choices[0].message.tool_calls).toHaveLength(1);
|
|
117
|
+
expect(response.choices[0].message.tool_calls[0].function.name).toBe(
|
|
118
|
+
"read_file",
|
|
119
|
+
);
|
|
120
|
+
expect(response.choices[0].message.tool_calls[0].function.arguments).toBe(
|
|
121
|
+
JSON.stringify({ path: "test.txt" }),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("creates default service when defaultService is null", async () => {
|
|
126
|
+
setTransformersServiceForTests(null);
|
|
127
|
+
|
|
128
|
+
const response = await executeNodeTransformersCompletion({
|
|
129
|
+
modelId: "onnx-community/Qwen3-0.6B-ONNX",
|
|
130
|
+
messages: [{ role: "user", content: "Test default" }],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(mockCreateTransformersRuntimeService).toHaveBeenCalled();
|
|
134
|
+
expect(response.choices[0].message.content).toBe(
|
|
135
|
+
"Created service default output",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("uses custom injected service when provided in options", async () => {
|
|
140
|
+
const customService: TransformersRuntimeService = {
|
|
141
|
+
runChatCompletion: jest.fn(async () => ({
|
|
142
|
+
text: "Custom service response",
|
|
143
|
+
promptTokens: 3,
|
|
144
|
+
completionTokens: 4,
|
|
145
|
+
})),
|
|
146
|
+
} as unknown as TransformersRuntimeService;
|
|
147
|
+
|
|
148
|
+
const response = await executeNodeTransformersCompletion({
|
|
149
|
+
modelId: "onnx-community/Qwen3-0.6B-ONNX",
|
|
150
|
+
messages: [{ role: "user", content: "Custom" }],
|
|
151
|
+
service: customService,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(customService.runChatCompletion).toHaveBeenCalled();
|
|
155
|
+
expect(response.choices[0].message.content).toBe("Custom service response");
|
|
156
|
+
});
|
|
58
157
|
});
|