@runfusion/fusion 0.12.0 → 0.14.0
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/README.md +13 -0
- package/dist/bin.js +1707 -610
- package/dist/client/assets/AgentDetailView-CBFUveyO.js +18 -0
- package/dist/client/assets/AgentsView-DPezXQ-U.js +522 -0
- package/dist/client/assets/{AgentsView-Bkk-uBij.css → AgentsView-V5GhlBYu.css} +1 -1
- package/dist/client/assets/ChatView-5N4-EuhD.js +1 -0
- package/dist/client/assets/{DevServerView-DQrVLbK5.js → DevServerView-Daft4YFc.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-DVmy6sLM.js → DirectoryPicker-rew1y6qO.js} +1 -1
- package/dist/client/assets/{DocumentsView-DHEv-Q2a.js → DocumentsView-i72qJzwd.js} +1 -1
- package/dist/client/assets/{InsightsView-ByyY7GX7.js → InsightsView-BL5eZJ0a.js} +3 -3
- package/dist/client/assets/{MemoryView-Udiu0u8R.js → MemoryView-pl8Cdg_p.js} +2 -2
- package/dist/client/assets/{NodesView-CupS-GGc.js → NodesView-D6eJ15zc.js} +4 -4
- package/dist/client/assets/PiExtensionsManager-ExInwXWP.js +11 -0
- package/dist/client/assets/PluginManager-CYhtxHun.js +1 -0
- package/dist/client/assets/{ResearchView-BG9Feaeb.js → ResearchView-B_QPUEjB.js} +1 -1
- package/dist/client/assets/{RoadmapsView-BTJtmBnF.js → RoadmapsView-DBNLaEsK.js} +2 -2
- package/dist/client/assets/SettingsModal-1ET586M3.js +31 -0
- package/dist/client/assets/{SettingsModal-eNCZiHa6.js → SettingsModal-CL_gWmOj.js} +1 -1
- package/dist/client/assets/SettingsModal-D_AFkDJa.css +1 -0
- package/dist/client/assets/{SetupWizardModal-yf79TN1L.js → SetupWizardModal-CLkY9HFL.js} +1 -1
- package/dist/client/assets/{SkillMultiselect-DOj5vX4U.js → SkillMultiselect-B0qi32SQ.js} +1 -1
- package/dist/client/assets/{SkillsView-CgnCnikX.js → SkillsView-umVjRq6o.js} +1 -1
- package/dist/client/assets/TodoView-CFifSvrD.js +6 -0
- package/dist/client/assets/TodoView-SeO9o7km.css +1 -0
- package/dist/client/assets/{folder-open-D11gjHGK.js → folder-open-nYPrL1W3.js} +1 -1
- package/dist/client/assets/index-Bc8nfKeH.js +661 -0
- package/dist/client/assets/index-C1prPuSl.css +1 -0
- package/dist/client/assets/{list-checks-CBzPc3GA.js → list-checks-sK8xJeH_.js} +1 -1
- package/dist/client/assets/{star-BWcRk8nt.js → star-BRtXbYkB.js} +1 -1
- package/dist/client/assets/{upload-91TM4ljC.js → upload-BP60eBwN.js} +1 -1
- package/dist/client/assets/{users-BAsI___L.js → users-qSGAX2Pf.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +6 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/index.ts +127 -0
- package/dist/droid-cli/package.json +37 -0
- package/dist/droid-cli/src/__tests__/control-handler.test.ts +164 -0
- package/dist/droid-cli/src/__tests__/event-bridge.test.ts +1318 -0
- package/dist/droid-cli/src/__tests__/mcp-config.test.ts +310 -0
- package/dist/droid-cli/src/__tests__/process-manager.test.ts +818 -0
- package/dist/droid-cli/src/__tests__/prompt-builder.test.ts +1206 -0
- package/dist/droid-cli/src/__tests__/provider.test.ts +1894 -0
- package/dist/droid-cli/src/__tests__/setup-test-isolation.test.ts +32 -0
- package/dist/droid-cli/src/__tests__/setup-test-isolation.ts +14 -0
- package/dist/droid-cli/src/__tests__/stream-parser.test.ts +188 -0
- package/dist/droid-cli/src/__tests__/thinking-config.test.ts +141 -0
- package/dist/droid-cli/src/__tests__/tool-mapping.test.ts +253 -0
- package/dist/droid-cli/src/control-handler.ts +82 -0
- package/dist/droid-cli/src/event-bridge.ts +397 -0
- package/dist/droid-cli/src/mcp-config.ts +144 -0
- package/dist/droid-cli/src/mcp-schema-server.cjs +49 -0
- package/dist/droid-cli/src/process-manager.ts +358 -0
- package/dist/droid-cli/src/prompt-builder.ts +629 -0
- package/dist/droid-cli/src/provider.ts +447 -0
- package/dist/droid-cli/src/stream-parser.ts +37 -0
- package/dist/droid-cli/src/thinking-config.ts +83 -0
- package/dist/droid-cli/src/tool-mapping.ts +147 -0
- package/dist/droid-cli/src/types.ts +87 -0
- package/dist/extension.js +542 -141
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +36 -0
- package/dist/pi-claude-cli/src/prompt-builder.ts +19 -28
- package/package.json +2 -1
- package/dist/client/assets/AgentDetailView-B20ApPe1.js +0 -18
- package/dist/client/assets/AgentsView-ChN1tgQ0.js +0 -522
- package/dist/client/assets/ChatView-oPMFwmoc.js +0 -1
- package/dist/client/assets/PiExtensionsManager-DXs2xI8K.js +0 -11
- package/dist/client/assets/PluginManager-BCpiZf4_.js +0 -1
- package/dist/client/assets/SettingsModal-9HS8MnmW.css +0 -1
- package/dist/client/assets/SettingsModal-DZ_LaEhd.js +0 -31
- package/dist/client/assets/TodoView-67BMyICY.js +0 -6
- package/dist/client/assets/TodoView-C1g65hJo.css +0 -1
- package/dist/client/assets/index-BLn1R7Ob.css +0 -1
- package/dist/client/assets/index-CLAHcGnI.js +0 -656
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const TEMP_HOME_PREFIX = "fn-test-home-";
|
|
6
|
+
|
|
7
|
+
describe("test isolation setup", () => {
|
|
8
|
+
it("overrides process.env.HOME to a temp directory", () => {
|
|
9
|
+
const home = process.env.HOME;
|
|
10
|
+
const userProfile = process.env.USERPROFILE;
|
|
11
|
+
|
|
12
|
+
expect(home).toBeDefined();
|
|
13
|
+
expect(home).toContain(tmpdir());
|
|
14
|
+
expect(home).toContain(TEMP_HOME_PREFIX);
|
|
15
|
+
expect(userProfile).toBe(home);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("resolves homedir() to the temp HOME", () => {
|
|
19
|
+
const home = homedir();
|
|
20
|
+
|
|
21
|
+
expect(home).toContain(tmpdir());
|
|
22
|
+
expect(home).toContain(TEMP_HOME_PREFIX);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("resolves ~/.pi/agent/AGENTS.md under the temp HOME", () => {
|
|
26
|
+
const agentsPath = join(homedir(), ".pi", "agent", "AGENTS.md");
|
|
27
|
+
|
|
28
|
+
expect(agentsPath).toContain(tmpdir());
|
|
29
|
+
expect(agentsPath).toContain(TEMP_HOME_PREFIX);
|
|
30
|
+
expect(agentsPath).toMatch(/fn-test-home-.*[\\/]\.pi[\\/]agent[\\/]AGENTS\.md$/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const tempHome = mkdtempSync(join(tmpdir(), "fn-test-home-"));
|
|
6
|
+
process.env.HOME = tempHome;
|
|
7
|
+
process.env.USERPROFILE = tempHome;
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
const match = tempHome.match(/^([A-Za-z]:)(.*)$/);
|
|
10
|
+
if (match) {
|
|
11
|
+
process.env.HOMEDRIVE = match[1];
|
|
12
|
+
process.env.HOMEPATH = match[2] || "\\";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { parseLine } from "../stream-parser";
|
|
3
|
+
import type {
|
|
4
|
+
ClaudeStreamEventMessage,
|
|
5
|
+
ClaudeResultMessage,
|
|
6
|
+
ClaudeSystemMessage,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
describe("parseLine", () => {
|
|
10
|
+
describe("valid JSON parsing", () => {
|
|
11
|
+
it("parses a valid stream_event message", () => {
|
|
12
|
+
const line = JSON.stringify({
|
|
13
|
+
type: "stream_event",
|
|
14
|
+
event: {
|
|
15
|
+
type: "message_start",
|
|
16
|
+
message: { usage: { input_tokens: 10 } },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const result = parseLine(line);
|
|
20
|
+
expect(result).not.toBeNull();
|
|
21
|
+
expect(result!.type).toBe("stream_event");
|
|
22
|
+
expect((result as ClaudeStreamEventMessage).event.type).toBe(
|
|
23
|
+
"message_start",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("parses a valid result message", () => {
|
|
28
|
+
const line = JSON.stringify({
|
|
29
|
+
type: "result",
|
|
30
|
+
subtype: "success",
|
|
31
|
+
result: "Hello world",
|
|
32
|
+
});
|
|
33
|
+
const result = parseLine(line);
|
|
34
|
+
expect(result).not.toBeNull();
|
|
35
|
+
expect(result!.type).toBe("result");
|
|
36
|
+
expect((result as ClaudeResultMessage).subtype).toBe("success");
|
|
37
|
+
expect((result as ClaudeResultMessage).result).toBe("Hello world");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("parses a valid system message", () => {
|
|
41
|
+
const line = JSON.stringify({
|
|
42
|
+
type: "system",
|
|
43
|
+
subtype: "init",
|
|
44
|
+
session_id: "test-session",
|
|
45
|
+
});
|
|
46
|
+
const result = parseLine(line);
|
|
47
|
+
expect(result).not.toBeNull();
|
|
48
|
+
expect(result!.type).toBe("system");
|
|
49
|
+
expect((result as ClaudeSystemMessage).subtype).toBe("init");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("parses a valid control_request message", () => {
|
|
53
|
+
const line = JSON.stringify({
|
|
54
|
+
type: "control_request",
|
|
55
|
+
request_id: "req-001",
|
|
56
|
+
request: {
|
|
57
|
+
subtype: "can_use_tool",
|
|
58
|
+
tool_name: "Read",
|
|
59
|
+
input: { file_path: "/test" },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const result = parseLine(line);
|
|
63
|
+
expect(result).not.toBeNull();
|
|
64
|
+
expect(result!.type).toBe("control_request");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("empty and whitespace lines", () => {
|
|
69
|
+
it("returns null for empty string", () => {
|
|
70
|
+
expect(parseLine("")).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns null for whitespace-only line", () => {
|
|
74
|
+
expect(parseLine(" ")).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns null for tab-only line", () => {
|
|
78
|
+
expect(parseLine("\t\t")).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns null for newline-only line", () => {
|
|
82
|
+
expect(parseLine("\n")).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("non-JSON lines (debug noise)", () => {
|
|
87
|
+
it("returns null for SandboxDebug output", () => {
|
|
88
|
+
expect(parseLine("[SandboxDebug] loading config...")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns null for plain text", () => {
|
|
92
|
+
expect(parseLine("Some debug message")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns null for lines starting with [", () => {
|
|
96
|
+
expect(parseLine("[INFO] starting up")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns null for lines starting with #", () => {
|
|
100
|
+
expect(parseLine("# comment")).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("malformed JSON", () => {
|
|
105
|
+
it("returns null for truncated JSON without throwing", () => {
|
|
106
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
107
|
+
expect(parseLine('{"type":"stream_event","event":')).toBeNull();
|
|
108
|
+
spy.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns null for invalid JSON syntax without throwing", () => {
|
|
112
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
113
|
+
expect(parseLine("{not valid json}")).toBeNull();
|
|
114
|
+
spy.mockRestore();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns null for JSON with trailing comma without throwing", () => {
|
|
118
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
119
|
+
expect(parseLine('{"type":"test",}')).toBeNull();
|
|
120
|
+
spy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("non-object JSON", () => {
|
|
125
|
+
it("returns null for JSON array", () => {
|
|
126
|
+
expect(parseLine("[1, 2, 3]")).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns null for JSON string", () => {
|
|
130
|
+
expect(parseLine('"hello"')).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns null for JSON number", () => {
|
|
134
|
+
expect(parseLine("42")).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns null for JSON null", () => {
|
|
138
|
+
expect(parseLine("null")).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns null for JSON boolean", () => {
|
|
142
|
+
expect(parseLine("true")).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("whitespace handling", () => {
|
|
147
|
+
it("trims leading whitespace before parsing", () => {
|
|
148
|
+
const line = ` ${JSON.stringify({ type: "system", subtype: "init" })}`;
|
|
149
|
+
const result = parseLine(line);
|
|
150
|
+
expect(result).not.toBeNull();
|
|
151
|
+
expect(result!.type).toBe("system");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("trims trailing whitespace before parsing", () => {
|
|
155
|
+
const line = `${JSON.stringify({ type: "system", subtype: "init" })} `;
|
|
156
|
+
const result = parseLine(line);
|
|
157
|
+
expect(result).not.toBeNull();
|
|
158
|
+
expect(result!.type).toBe("system");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("trims both leading and trailing whitespace", () => {
|
|
162
|
+
const line = ` ${JSON.stringify({ type: "result", subtype: "success" })} `;
|
|
163
|
+
const result = parseLine(line);
|
|
164
|
+
expect(result).not.toBeNull();
|
|
165
|
+
expect(result!.type).toBe("result");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("resilience", () => {
|
|
170
|
+
it("never throws regardless of input", () => {
|
|
171
|
+
const inputs = [
|
|
172
|
+
"",
|
|
173
|
+
" ",
|
|
174
|
+
"garbage",
|
|
175
|
+
"{bad",
|
|
176
|
+
"null",
|
|
177
|
+
"undefined",
|
|
178
|
+
"[1,2]",
|
|
179
|
+
'{"valid": true}',
|
|
180
|
+
"[SandboxDebug] test",
|
|
181
|
+
'{"type":"stream_event","event":{"type":"message_start"}}',
|
|
182
|
+
];
|
|
183
|
+
for (const input of inputs) {
|
|
184
|
+
expect(() => parseLine(input)).not.toThrow();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { mapThinkingEffort, isOpusModel } from "../thinking-config";
|
|
3
|
+
import type { ThinkingBudgets } from "@mariozechner/pi-ai";
|
|
4
|
+
|
|
5
|
+
describe("isOpusModel", () => {
|
|
6
|
+
it("returns true for claude-opus-4-6-20260301", () => {
|
|
7
|
+
expect(isOpusModel("claude-opus-4-6-20260301")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns false for claude-sonnet-4-5-20250929", () => {
|
|
11
|
+
expect(isOpusModel("claude-sonnet-4-5-20250929")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns true for future Opus models (forward-compatible)", () => {
|
|
15
|
+
expect(isOpusModel("claude-opus-5-20270101")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns false for non-Opus model strings", () => {
|
|
19
|
+
expect(isOpusModel("claude-haiku-3-5-20240307")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("mapThinkingEffort", () => {
|
|
24
|
+
describe("undefined reasoning", () => {
|
|
25
|
+
it("returns undefined when reasoning is undefined", () => {
|
|
26
|
+
expect(
|
|
27
|
+
mapThinkingEffort(undefined, "claude-sonnet-4-5", undefined),
|
|
28
|
+
).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns undefined regardless of model", () => {
|
|
32
|
+
expect(
|
|
33
|
+
mapThinkingEffort(undefined, "claude-opus-4-6-20260301", undefined),
|
|
34
|
+
).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("standard (non-Opus) model mapping", () => {
|
|
39
|
+
const model = "claude-sonnet-4-5";
|
|
40
|
+
|
|
41
|
+
it("maps minimal to low", () => {
|
|
42
|
+
expect(mapThinkingEffort("minimal", model, undefined)).toBe("low");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("maps low to low", () => {
|
|
46
|
+
expect(mapThinkingEffort("low", model, undefined)).toBe("low");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("maps medium to medium", () => {
|
|
50
|
+
expect(mapThinkingEffort("medium", model, undefined)).toBe("medium");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("maps high to high", () => {
|
|
54
|
+
expect(mapThinkingEffort("high", model, undefined)).toBe("high");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("maps xhigh to high (downgrade for non-Opus)", () => {
|
|
58
|
+
expect(mapThinkingEffort("xhigh", model, undefined)).toBe("high");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("Opus model mapping (elevated)", () => {
|
|
63
|
+
const model = "claude-opus-4-6-20260301";
|
|
64
|
+
|
|
65
|
+
it("maps minimal to low", () => {
|
|
66
|
+
expect(mapThinkingEffort("minimal", model, undefined)).toBe("low");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("maps low to low", () => {
|
|
70
|
+
expect(mapThinkingEffort("low", model, undefined)).toBe("low");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("maps medium to high (shifted up)", () => {
|
|
74
|
+
expect(mapThinkingEffort("medium", model, undefined)).toBe("high");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("maps high to max (shifted up)", () => {
|
|
78
|
+
expect(mapThinkingEffort("high", model, undefined)).toBe("max");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("maps xhigh to max", () => {
|
|
82
|
+
expect(mapThinkingEffort("xhigh", model, undefined)).toBe("max");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("thinkingBudgets warning", () => {
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.restoreAllMocks();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("logs console.warn when thinkingBudgets is provided with entries", () => {
|
|
92
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
93
|
+
const budgets: ThinkingBudgets = { high: 50000 };
|
|
94
|
+
|
|
95
|
+
mapThinkingEffort("high", "claude-sonnet-4-5", budgets);
|
|
96
|
+
|
|
97
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining("thinkingBudgets are not supported"),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not warn when thinkingBudgets is undefined", () => {
|
|
104
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
105
|
+
|
|
106
|
+
mapThinkingEffort("high", "claude-sonnet-4-5", undefined);
|
|
107
|
+
|
|
108
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not warn when thinkingBudgets is empty object", () => {
|
|
112
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
113
|
+
|
|
114
|
+
mapThinkingEffort("high", "claude-sonnet-4-5", {} as ThinkingBudgets);
|
|
115
|
+
|
|
116
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("still returns correct effort level when budgets trigger warning", () => {
|
|
120
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
121
|
+
const budgets: ThinkingBudgets = { high: 50000 };
|
|
122
|
+
|
|
123
|
+
const result = mapThinkingEffort(
|
|
124
|
+
"high",
|
|
125
|
+
"claude-opus-4-6-20260301",
|
|
126
|
+
budgets,
|
|
127
|
+
);
|
|
128
|
+
expect(result).toBe("max");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("no modelId defaults to non-Opus behavior", () => {
|
|
133
|
+
it("uses standard mapping when modelId is undefined", () => {
|
|
134
|
+
expect(mapThinkingEffort("medium", undefined, undefined)).toBe("medium");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not return max for xhigh when modelId is undefined", () => {
|
|
138
|
+
expect(mapThinkingEffort("xhigh", undefined, undefined)).toBe("high");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
TOOL_MAPPINGS,
|
|
4
|
+
CUSTOM_TOOLS_MCP_PREFIX,
|
|
5
|
+
mapDroidToolNameToPi,
|
|
6
|
+
mapPiToolNameToDroid,
|
|
7
|
+
translateDroidArgsToPi,
|
|
8
|
+
translatePiArgsToDroid,
|
|
9
|
+
isCustomToolName,
|
|
10
|
+
} from "../tool-mapping";
|
|
11
|
+
|
|
12
|
+
describe("tool-mapping", () => {
|
|
13
|
+
describe("TOOL_MAPPINGS", () => {
|
|
14
|
+
it("exports 6 tool mappings", () => {
|
|
15
|
+
expect(TOOL_MAPPINGS).toHaveLength(6);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("mapDroidToolNameToPi", () => {
|
|
20
|
+
it("maps Read to read", () => {
|
|
21
|
+
expect(mapDroidToolNameToPi("Read")).toBe("read");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("maps Write to write", () => {
|
|
25
|
+
expect(mapDroidToolNameToPi("Write")).toBe("write");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("maps Edit to edit", () => {
|
|
29
|
+
expect(mapDroidToolNameToPi("Edit")).toBe("edit");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("maps Bash to bash", () => {
|
|
33
|
+
expect(mapDroidToolNameToPi("Bash")).toBe("bash");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("maps Grep to grep", () => {
|
|
37
|
+
expect(mapDroidToolNameToPi("Grep")).toBe("grep");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("maps Glob to find", () => {
|
|
41
|
+
expect(mapDroidToolNameToPi("Glob")).toBe("find");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("passes through unknown tool names unchanged", () => {
|
|
45
|
+
expect(mapDroidToolNameToPi("UnknownTool")).toBe("UnknownTool");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("is case-insensitive for Claude tool names", () => {
|
|
49
|
+
expect(mapDroidToolNameToPi("read")).toBe("read");
|
|
50
|
+
expect(mapDroidToolNameToPi("READ")).toBe("read");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("mapPiToolNameToDroid", () => {
|
|
55
|
+
it("maps read to Read", () => {
|
|
56
|
+
expect(mapPiToolNameToDroid("read")).toBe("Read");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("maps write to Write", () => {
|
|
60
|
+
expect(mapPiToolNameToDroid("write")).toBe("Write");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("maps edit to Edit", () => {
|
|
64
|
+
expect(mapPiToolNameToDroid("edit")).toBe("Edit");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("maps bash to Bash", () => {
|
|
68
|
+
expect(mapPiToolNameToDroid("bash")).toBe("Bash");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("maps grep to Grep", () => {
|
|
72
|
+
expect(mapPiToolNameToDroid("grep")).toBe("Grep");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("maps find to Glob", () => {
|
|
76
|
+
expect(mapPiToolNameToDroid("find")).toBe("Glob");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("maps glob to Glob (asymmetry: both find and glob map to Glob)", () => {
|
|
80
|
+
expect(mapPiToolNameToDroid("glob")).toBe("Glob");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("passes through unknown tool names unchanged", () => {
|
|
84
|
+
expect(mapPiToolNameToDroid("unknownTool")).toBe("unknownTool");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("translateDroidArgsToPi", () => {
|
|
89
|
+
it("renames file_path to path for Read", () => {
|
|
90
|
+
const result = translateDroidArgsToPi("Read", {
|
|
91
|
+
file_path: "/foo",
|
|
92
|
+
offset: 10,
|
|
93
|
+
});
|
|
94
|
+
expect(result).toEqual({ path: "/foo", offset: 10 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renames file_path to path for Write", () => {
|
|
98
|
+
const result = translateDroidArgsToPi("Write", {
|
|
99
|
+
file_path: "/bar",
|
|
100
|
+
content: "hello",
|
|
101
|
+
});
|
|
102
|
+
expect(result).toEqual({ path: "/bar", content: "hello" });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renames file_path, old_string, new_string for Edit", () => {
|
|
106
|
+
const result = translateDroidArgsToPi("Edit", {
|
|
107
|
+
file_path: "/f",
|
|
108
|
+
old_string: "a",
|
|
109
|
+
new_string: "b",
|
|
110
|
+
});
|
|
111
|
+
expect(result).toEqual({ path: "/f", oldText: "a", newText: "b" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("passes through Bash args unchanged (no renames)", () => {
|
|
115
|
+
const result = translateDroidArgsToPi("Bash", { command: "ls" });
|
|
116
|
+
expect(result).toEqual({ command: "ls" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("renames head_limit to limit for Grep", () => {
|
|
120
|
+
const result = translateDroidArgsToPi("Grep", {
|
|
121
|
+
pattern: "x",
|
|
122
|
+
head_limit: 5,
|
|
123
|
+
});
|
|
124
|
+
expect(result).toEqual({ pattern: "x", limit: 5 });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("passes through Glob args unchanged (no renames)", () => {
|
|
128
|
+
const result = translateDroidArgsToPi("Glob", { pattern: "*.ts" });
|
|
129
|
+
expect(result).toEqual({ pattern: "*.ts" });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("passes through args for unknown tools unchanged", () => {
|
|
133
|
+
const result = translateDroidArgsToPi("UnknownTool", {
|
|
134
|
+
foo: 1,
|
|
135
|
+
bar: "baz",
|
|
136
|
+
});
|
|
137
|
+
expect(result).toEqual({ foo: 1, bar: "baz" });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("preserves unknown args alongside renamed args", () => {
|
|
141
|
+
const result = translateDroidArgsToPi("Read", {
|
|
142
|
+
file_path: "/foo",
|
|
143
|
+
offset: 10,
|
|
144
|
+
limit: 50,
|
|
145
|
+
extra_arg: true,
|
|
146
|
+
});
|
|
147
|
+
expect(result).toEqual({
|
|
148
|
+
path: "/foo",
|
|
149
|
+
offset: 10,
|
|
150
|
+
limit: 50,
|
|
151
|
+
extra_arg: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("translatePiArgsToDroid", () => {
|
|
157
|
+
it("renames path to file_path for read", () => {
|
|
158
|
+
const result = translatePiArgsToDroid("read", { path: "/foo" });
|
|
159
|
+
expect(result).toEqual({ file_path: "/foo" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("renames path, oldText, newText for edit", () => {
|
|
163
|
+
const result = translatePiArgsToDroid("edit", {
|
|
164
|
+
path: "/f",
|
|
165
|
+
oldText: "a",
|
|
166
|
+
newText: "b",
|
|
167
|
+
});
|
|
168
|
+
expect(result).toEqual({
|
|
169
|
+
file_path: "/f",
|
|
170
|
+
old_string: "a",
|
|
171
|
+
new_string: "b",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("renames limit to head_limit for grep", () => {
|
|
176
|
+
const result = translatePiArgsToDroid("grep", {
|
|
177
|
+
pattern: "x",
|
|
178
|
+
limit: 5,
|
|
179
|
+
});
|
|
180
|
+
expect(result).toEqual({ pattern: "x", head_limit: 5 });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("passes through unknown args alongside renamed args", () => {
|
|
184
|
+
const result = translatePiArgsToDroid("read", {
|
|
185
|
+
path: "/foo",
|
|
186
|
+
offset: 10,
|
|
187
|
+
extra: "val",
|
|
188
|
+
});
|
|
189
|
+
expect(result).toEqual({ file_path: "/foo", offset: 10, extra: "val" });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("passes through args for unknown tools unchanged", () => {
|
|
193
|
+
const result = translatePiArgsToDroid("unknownTool", { foo: 1 });
|
|
194
|
+
expect(result).toEqual({ foo: 1 });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("MCP prefix stripping", () => {
|
|
199
|
+
it("strips mcp__custom-tools__ prefix from myTool", () => {
|
|
200
|
+
expect(mapDroidToolNameToPi("mcp__custom-tools__myTool")).toBe("myTool");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("strips mcp__custom-tools__ prefix from deploy", () => {
|
|
204
|
+
expect(mapDroidToolNameToPi("mcp__custom-tools__deploy")).toBe("deploy");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("handles empty name after prefix", () => {
|
|
208
|
+
expect(mapDroidToolNameToPi("mcp__custom-tools__")).toBe("");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does NOT strip other MCP server prefixes", () => {
|
|
212
|
+
expect(mapDroidToolNameToPi("mcp__other-server__foo")).toBe(
|
|
213
|
+
"mcp__other-server__foo",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("built-in mappings still work alongside MCP prefix stripping", () => {
|
|
218
|
+
expect(mapDroidToolNameToPi("Read")).toBe("read");
|
|
219
|
+
expect(mapDroidToolNameToPi("Glob")).toBe("find");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("CUSTOM_TOOLS_MCP_PREFIX is the correct string", () => {
|
|
223
|
+
expect(CUSTOM_TOOLS_MCP_PREFIX).toBe("mcp__custom-tools__");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("isCustomToolName", () => {
|
|
228
|
+
it("returns true for custom tool names", () => {
|
|
229
|
+
expect(isCustomToolName("myTool")).toBe(true);
|
|
230
|
+
expect(isCustomToolName("deploy")).toBe(true);
|
|
231
|
+
expect(isCustomToolName("ls")).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns false for all 6 built-in tool names", () => {
|
|
235
|
+
expect(isCustomToolName("read")).toBe(false);
|
|
236
|
+
expect(isCustomToolName("write")).toBe(false);
|
|
237
|
+
expect(isCustomToolName("edit")).toBe(false);
|
|
238
|
+
expect(isCustomToolName("bash")).toBe(false);
|
|
239
|
+
expect(isCustomToolName("grep")).toBe(false);
|
|
240
|
+
expect(isCustomToolName("find")).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("translateDroidArgsToPi with MCP prefix", () => {
|
|
245
|
+
it("MCP-prefixed custom tool args pass through unchanged", () => {
|
|
246
|
+
const result = translateDroidArgsToPi("mcp__custom-tools__myTool", {
|
|
247
|
+
foo: 1,
|
|
248
|
+
bar: "baz",
|
|
249
|
+
});
|
|
250
|
+
expect(result).toEqual({ foo: 1, bar: "baz" });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|