@dexto/tools-filesystem 1.7.1 → 1.8.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/dist/directory-approval.cjs +5 -4
- package/dist/directory-approval.d.ts +2 -1
- package/dist/directory-approval.d.ts.map +1 -1
- package/dist/directory-approval.integration.test.cjs +73 -33
- package/dist/directory-approval.integration.test.js +73 -33
- package/dist/directory-approval.js +2 -1
- package/dist/edit-file-tool.cjs +52 -37
- package/dist/edit-file-tool.d.ts +1 -1
- package/dist/edit-file-tool.d.ts.map +1 -1
- package/dist/edit-file-tool.js +43 -29
- package/dist/edit-file-tool.test.cjs +159 -2
- package/dist/edit-file-tool.test.js +159 -2
- package/dist/errors.cjs +53 -53
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/file-tool-types.d.ts +1 -1
- package/dist/file-tool-types.d.ts.map +1 -1
- package/dist/filesystem-service.cjs +60 -60
- package/dist/filesystem-service.d.ts +1 -1
- package/dist/filesystem-service.d.ts.map +1 -1
- package/dist/filesystem-service.js +5 -5
- package/dist/filesystem-service.test.cjs +1 -3
- package/dist/filesystem-service.test.js +1 -3
- package/dist/glob-files-tool.cjs +27 -24
- package/dist/glob-files-tool.d.ts +1 -1
- package/dist/glob-files-tool.d.ts.map +1 -1
- package/dist/glob-files-tool.js +24 -21
- package/dist/glob-files-tool.test.cjs +100 -88
- package/dist/glob-files-tool.test.js +101 -67
- package/dist/grep-content-tool.cjs +129 -44
- package/dist/grep-content-tool.d.ts +1 -1
- package/dist/grep-content-tool.d.ts.map +1 -1
- package/dist/grep-content-tool.js +120 -41
- package/dist/grep-content-tool.test.cjs +122 -87
- package/dist/grep-content-tool.test.js +123 -66
- package/dist/index.d.cts +3 -4
- package/dist/path-validator.d.ts +1 -1
- package/dist/path-validator.d.ts.map +1 -1
- package/dist/read-file-tool.cjs +43 -14
- package/dist/read-file-tool.d.ts +1 -1
- package/dist/read-file-tool.d.ts.map +1 -1
- package/dist/read-file-tool.js +40 -11
- package/dist/read-file-tool.test.cjs +119 -0
- package/dist/read-file-tool.test.d.ts +2 -0
- package/dist/read-file-tool.test.d.ts.map +1 -0
- package/dist/read-file-tool.test.js +96 -0
- package/dist/read-media-file-tool.cjs +4 -4
- package/dist/read-media-file-tool.d.ts +1 -1
- package/dist/read-media-file-tool.d.ts.map +1 -1
- package/dist/read-media-file-tool.js +1 -1
- package/dist/tool-factory.cjs +2 -2
- package/dist/tool-factory.js +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace-paths.cjs +87 -0
- package/dist/workspace-paths.d.ts +4 -0
- package/dist/workspace-paths.d.ts.map +1 -0
- package/dist/workspace-paths.js +51 -0
- package/dist/write-file-tool.cjs +74 -34
- package/dist/write-file-tool.d.ts +1 -2
- package/dist/write-file-tool.d.ts.map +1 -1
- package/dist/write-file-tool.js +68 -29
- package/dist/write-file-tool.test.cjs +262 -11
- package/dist/write-file-tool.test.js +262 -11
- package/package.json +3 -3
package/dist/edit-file-tool.js
CHANGED
|
@@ -3,14 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
import { createPatch } from "diff";
|
|
4
4
|
import {
|
|
5
5
|
createLocalToolCallHeader,
|
|
6
|
-
DextoRuntimeError,
|
|
7
6
|
ToolError,
|
|
8
7
|
ToolErrorCode,
|
|
9
8
|
defineTool,
|
|
10
9
|
truncateForHeader
|
|
11
|
-
} from "@dexto/core";
|
|
10
|
+
} from "@dexto/core/tools";
|
|
11
|
+
import { DextoRuntimeError } from "@dexto/core/errors";
|
|
12
12
|
import { FileSystemErrorCode } from "./error-codes.js";
|
|
13
13
|
import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
|
|
14
|
+
import { toWorkspaceRelativePath } from "./workspace-paths.js";
|
|
14
15
|
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
15
16
|
function computeContentHash(content) {
|
|
16
17
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
@@ -51,7 +52,7 @@ function createEditFileTool(getFileSystemService) {
|
|
|
51
52
|
}),
|
|
52
53
|
presentation: {
|
|
53
54
|
describeHeader: (input) => createLocalToolCallHeader({
|
|
54
|
-
title: "
|
|
55
|
+
title: "Edit",
|
|
55
56
|
argsText: truncateForHeader(input.file_path, 140)
|
|
56
57
|
}),
|
|
57
58
|
/**
|
|
@@ -124,54 +125,67 @@ function createEditFileTool(getFileSystemService) {
|
|
|
124
125
|
}
|
|
125
126
|
},
|
|
126
127
|
async execute(input, context) {
|
|
127
|
-
const resolvedFileSystemService = await getFileSystemService(context);
|
|
128
128
|
const { file_path, old_string, new_string, replace_all } = input;
|
|
129
|
-
const
|
|
130
|
-
|
|
129
|
+
const handle = await openWorkspace(context, "edit_file");
|
|
130
|
+
const workspacePath = toWorkspaceRelativePath(
|
|
131
|
+
"edit_file",
|
|
132
|
+
handle.context.path,
|
|
131
133
|
file_path
|
|
132
134
|
);
|
|
135
|
+
let currentContent = await handle.files.readText(workspacePath);
|
|
133
136
|
const toolCallId = context.toolCallId;
|
|
134
137
|
if (toolCallId) {
|
|
135
138
|
const expectedHash = previewContentHashCache.get(toolCallId);
|
|
136
139
|
if (expectedHash === void 0) {
|
|
137
140
|
} else {
|
|
138
141
|
previewContentHashCache.delete(toolCallId);
|
|
139
|
-
let currentContent;
|
|
140
142
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
145
|
-
throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
|
|
146
|
-
}
|
|
147
|
-
throw error;
|
|
143
|
+
currentContent = await handle.files.readText(workspacePath);
|
|
144
|
+
} catch {
|
|
145
|
+
throw ToolError.fileModifiedSincePreview("edit_file", file_path);
|
|
148
146
|
}
|
|
149
147
|
const currentHash = computeContentHash(currentContent);
|
|
150
148
|
if (expectedHash !== currentHash) {
|
|
151
|
-
throw ToolError.fileModifiedSincePreview("edit_file",
|
|
149
|
+
throw ToolError.fileModifiedSincePreview("edit_file", file_path);
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
152
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
const occurrences = currentContent.split(old_string).length - 1;
|
|
154
|
+
if (occurrences === 0) {
|
|
155
|
+
throw ToolError.validationFailed(
|
|
156
|
+
"edit_file",
|
|
157
|
+
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
158
|
+
{
|
|
159
|
+
file_path,
|
|
160
|
+
old_string_preview: old_string.slice(0, 100)
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (!replace_all && occurrences > 1) {
|
|
165
|
+
throw ToolError.validationFailed(
|
|
166
|
+
"edit_file",
|
|
167
|
+
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
168
|
+
{ file_path, occurrences }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const newContent = replace_all ? currentContent.split(old_string).join(new_string) : currentContent.replace(old_string, new_string);
|
|
172
|
+
await handle.files.writeFile(workspacePath, newContent);
|
|
173
|
+
const _display = generateDiffPreview(file_path, currentContent, newContent);
|
|
165
174
|
return {
|
|
166
|
-
success:
|
|
167
|
-
path:
|
|
168
|
-
changes_count:
|
|
169
|
-
...result.backupPath && { backup_path: result.backupPath },
|
|
175
|
+
success: true,
|
|
176
|
+
path: file_path,
|
|
177
|
+
changes_count: occurrences,
|
|
170
178
|
_display
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
181
|
});
|
|
174
182
|
}
|
|
183
|
+
async function openWorkspace(context, toolName) {
|
|
184
|
+
if (!context.services) {
|
|
185
|
+
throw new Error(`${toolName} requires ToolExecutionContext.services`);
|
|
186
|
+
}
|
|
187
|
+
return context.services.workspaceManager.open({ intent: "write" });
|
|
188
|
+
}
|
|
175
189
|
export {
|
|
176
190
|
createEditFileTool
|
|
177
191
|
};
|
|
@@ -46,8 +46,49 @@ const createMockLogger = () => {
|
|
|
46
46
|
};
|
|
47
47
|
return logger;
|
|
48
48
|
};
|
|
49
|
-
function createToolContext(logger, overrides = {}) {
|
|
50
|
-
return {
|
|
49
|
+
function createToolContext(logger, overrides = {}, workspaceRoot = currentWorkspaceRoot) {
|
|
50
|
+
return {
|
|
51
|
+
logger,
|
|
52
|
+
services: createWorkspaceServices(workspaceRoot),
|
|
53
|
+
...overrides
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let currentWorkspaceRoot = process.cwd();
|
|
57
|
+
function createWorkspaceServices(workspaceRoot) {
|
|
58
|
+
const workspaceManager = {
|
|
59
|
+
open: import_vitest.vi.fn(async () => ({
|
|
60
|
+
context: {
|
|
61
|
+
id: "test-workspace",
|
|
62
|
+
path: workspaceRoot,
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
lastActiveAt: Date.now()
|
|
65
|
+
},
|
|
66
|
+
capabilities: ["files"],
|
|
67
|
+
files: {
|
|
68
|
+
readFile: async (filePath) => fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8"),
|
|
69
|
+
readText: async (filePath) => fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8"),
|
|
70
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
71
|
+
writeFile: async (filePath, content) => {
|
|
72
|
+
const resolvedPath = resolveWorkspacePath(workspaceRoot, filePath);
|
|
73
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
74
|
+
},
|
|
75
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
76
|
+
}
|
|
77
|
+
}))
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
approval: {},
|
|
81
|
+
search: {},
|
|
82
|
+
resources: {},
|
|
83
|
+
prompts: {},
|
|
84
|
+
skills: {},
|
|
85
|
+
mcp: {},
|
|
86
|
+
taskForker: null,
|
|
87
|
+
workspaceManager
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
91
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
51
92
|
}
|
|
52
93
|
(0, import_vitest.describe)("edit_file tool", () => {
|
|
53
94
|
let mockLogger;
|
|
@@ -70,6 +111,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
70
111
|
mockLogger
|
|
71
112
|
);
|
|
72
113
|
await fileSystemService.initialize();
|
|
114
|
+
currentWorkspaceRoot = tempDir;
|
|
73
115
|
import_vitest.vi.clearAllMocks();
|
|
74
116
|
});
|
|
75
117
|
(0, import_vitest.afterEach)(async () => {
|
|
@@ -78,6 +120,121 @@ function createToolContext(logger, overrides = {}) {
|
|
|
78
120
|
} catch {
|
|
79
121
|
}
|
|
80
122
|
});
|
|
123
|
+
(0, import_vitest.it)("edits through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
124
|
+
await fs.writeFile(path.join(tempDir, "workspace.txt"), "hello world");
|
|
125
|
+
const getFileSystemService = import_vitest.vi.fn(async () => {
|
|
126
|
+
throw new Error("edit_file execute must not use FileSystemService");
|
|
127
|
+
});
|
|
128
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(getFileSystemService);
|
|
129
|
+
const parsedInput = tool.inputSchema.parse({
|
|
130
|
+
file_path: "workspace.txt",
|
|
131
|
+
old_string: "world",
|
|
132
|
+
new_string: "workspace"
|
|
133
|
+
});
|
|
134
|
+
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
135
|
+
(0, import_vitest.expect)(getFileSystemService).not.toHaveBeenCalled();
|
|
136
|
+
(0, import_vitest.expect)(result).toMatchObject({
|
|
137
|
+
success: true,
|
|
138
|
+
path: "workspace.txt",
|
|
139
|
+
changes_count: 1
|
|
140
|
+
});
|
|
141
|
+
await (0, import_vitest.expect)(fs.readFile(path.join(tempDir, "workspace.txt"), "utf-8")).resolves.toBe(
|
|
142
|
+
"hello workspace"
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
(0, import_vitest.it)("normalizes workspace-contained absolute paths before editing", async () => {
|
|
146
|
+
const readText = import_vitest.vi.fn(async () => "hello world");
|
|
147
|
+
const writeFile = import_vitest.vi.fn(async () => void 0);
|
|
148
|
+
const workspaceManager = {
|
|
149
|
+
open: import_vitest.vi.fn(async () => ({
|
|
150
|
+
context: {
|
|
151
|
+
id: "test-workspace",
|
|
152
|
+
path: tempDir,
|
|
153
|
+
createdAt: Date.now(),
|
|
154
|
+
lastActiveAt: Date.now()
|
|
155
|
+
},
|
|
156
|
+
capabilities: ["files"],
|
|
157
|
+
files: {
|
|
158
|
+
readFile: readText,
|
|
159
|
+
readText,
|
|
160
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
161
|
+
writeFile,
|
|
162
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
163
|
+
}
|
|
164
|
+
}))
|
|
165
|
+
};
|
|
166
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(import_vitest.vi.fn());
|
|
167
|
+
await tool.execute(
|
|
168
|
+
tool.inputSchema.parse({
|
|
169
|
+
file_path: path.join(tempDir, "workspace.txt"),
|
|
170
|
+
old_string: "world",
|
|
171
|
+
new_string: "workspace"
|
|
172
|
+
}),
|
|
173
|
+
createToolContext(mockLogger, {
|
|
174
|
+
services: {
|
|
175
|
+
...createWorkspaceServices(tempDir),
|
|
176
|
+
workspaceManager
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
(0, import_vitest.expect)(readText).toHaveBeenCalledWith("workspace.txt");
|
|
181
|
+
(0, import_vitest.expect)(writeFile).toHaveBeenCalledWith("workspace.txt", "hello workspace");
|
|
182
|
+
});
|
|
183
|
+
(0, import_vitest.it)("rejects external absolute paths before file provider calls", async () => {
|
|
184
|
+
const readText = import_vitest.vi.fn(async () => "hello world");
|
|
185
|
+
const writeFile = import_vitest.vi.fn(async () => void 0);
|
|
186
|
+
const workspaceManager = {
|
|
187
|
+
open: import_vitest.vi.fn(async () => ({
|
|
188
|
+
context: {
|
|
189
|
+
id: "test-workspace",
|
|
190
|
+
path: tempDir,
|
|
191
|
+
createdAt: Date.now(),
|
|
192
|
+
lastActiveAt: Date.now()
|
|
193
|
+
},
|
|
194
|
+
capabilities: ["files"],
|
|
195
|
+
files: {
|
|
196
|
+
readFile: readText,
|
|
197
|
+
readText,
|
|
198
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
199
|
+
writeFile,
|
|
200
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
201
|
+
}
|
|
202
|
+
}))
|
|
203
|
+
};
|
|
204
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(import_vitest.vi.fn());
|
|
205
|
+
await (0, import_vitest.expect)(
|
|
206
|
+
tool.execute(
|
|
207
|
+
tool.inputSchema.parse({
|
|
208
|
+
file_path: "/outside/workspace.txt",
|
|
209
|
+
old_string: "world",
|
|
210
|
+
new_string: "workspace"
|
|
211
|
+
}),
|
|
212
|
+
createToolContext(mockLogger, {
|
|
213
|
+
services: {
|
|
214
|
+
...createWorkspaceServices(tempDir),
|
|
215
|
+
workspaceManager
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
).rejects.toMatchObject({ code: import_core.ToolErrorCode.VALIDATION_FAILED });
|
|
220
|
+
(0, import_vitest.expect)(readText).not.toHaveBeenCalled();
|
|
221
|
+
(0, import_vitest.expect)(writeFile).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
(0, import_vitest.it)("should describe edit calls with an Edit header", () => {
|
|
224
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
225
|
+
const header = tool.presentation?.describeHeader?.(
|
|
226
|
+
tool.inputSchema.parse({
|
|
227
|
+
file_path: "snake.py",
|
|
228
|
+
old_string: "old",
|
|
229
|
+
new_string: "new"
|
|
230
|
+
}),
|
|
231
|
+
createToolContext(mockLogger)
|
|
232
|
+
);
|
|
233
|
+
(0, import_vitest.expect)(header).toEqual({
|
|
234
|
+
argsText: "snake.py",
|
|
235
|
+
title: "Edit"
|
|
236
|
+
});
|
|
237
|
+
});
|
|
81
238
|
(0, import_vitest.describe)("File Modification Detection", () => {
|
|
82
239
|
(0, import_vitest.it)("should generate preview for files outside config-allowed roots (preview read only)", async () => {
|
|
83
240
|
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
@@ -23,8 +23,49 @@ const createMockLogger = () => {
|
|
|
23
23
|
};
|
|
24
24
|
return logger;
|
|
25
25
|
};
|
|
26
|
-
function createToolContext(logger, overrides = {}) {
|
|
27
|
-
return {
|
|
26
|
+
function createToolContext(logger, overrides = {}, workspaceRoot = currentWorkspaceRoot) {
|
|
27
|
+
return {
|
|
28
|
+
logger,
|
|
29
|
+
services: createWorkspaceServices(workspaceRoot),
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let currentWorkspaceRoot = process.cwd();
|
|
34
|
+
function createWorkspaceServices(workspaceRoot) {
|
|
35
|
+
const workspaceManager = {
|
|
36
|
+
open: vi.fn(async () => ({
|
|
37
|
+
context: {
|
|
38
|
+
id: "test-workspace",
|
|
39
|
+
path: workspaceRoot,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
lastActiveAt: Date.now()
|
|
42
|
+
},
|
|
43
|
+
capabilities: ["files"],
|
|
44
|
+
files: {
|
|
45
|
+
readFile: async (filePath) => fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8"),
|
|
46
|
+
readText: async (filePath) => fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8"),
|
|
47
|
+
glob: vi.fn(async () => []),
|
|
48
|
+
writeFile: async (filePath, content) => {
|
|
49
|
+
const resolvedPath = resolveWorkspacePath(workspaceRoot, filePath);
|
|
50
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
51
|
+
},
|
|
52
|
+
listFiles: vi.fn(async () => [])
|
|
53
|
+
}
|
|
54
|
+
}))
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
approval: {},
|
|
58
|
+
search: {},
|
|
59
|
+
resources: {},
|
|
60
|
+
prompts: {},
|
|
61
|
+
skills: {},
|
|
62
|
+
mcp: {},
|
|
63
|
+
taskForker: null,
|
|
64
|
+
workspaceManager
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
68
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
28
69
|
}
|
|
29
70
|
describe("edit_file tool", () => {
|
|
30
71
|
let mockLogger;
|
|
@@ -47,6 +88,7 @@ describe("edit_file tool", () => {
|
|
|
47
88
|
mockLogger
|
|
48
89
|
);
|
|
49
90
|
await fileSystemService.initialize();
|
|
91
|
+
currentWorkspaceRoot = tempDir;
|
|
50
92
|
vi.clearAllMocks();
|
|
51
93
|
});
|
|
52
94
|
afterEach(async () => {
|
|
@@ -55,6 +97,121 @@ describe("edit_file tool", () => {
|
|
|
55
97
|
} catch {
|
|
56
98
|
}
|
|
57
99
|
});
|
|
100
|
+
it("edits through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
101
|
+
await fs.writeFile(path.join(tempDir, "workspace.txt"), "hello world");
|
|
102
|
+
const getFileSystemService = vi.fn(async () => {
|
|
103
|
+
throw new Error("edit_file execute must not use FileSystemService");
|
|
104
|
+
});
|
|
105
|
+
const tool = createEditFileTool(getFileSystemService);
|
|
106
|
+
const parsedInput = tool.inputSchema.parse({
|
|
107
|
+
file_path: "workspace.txt",
|
|
108
|
+
old_string: "world",
|
|
109
|
+
new_string: "workspace"
|
|
110
|
+
});
|
|
111
|
+
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
112
|
+
expect(getFileSystemService).not.toHaveBeenCalled();
|
|
113
|
+
expect(result).toMatchObject({
|
|
114
|
+
success: true,
|
|
115
|
+
path: "workspace.txt",
|
|
116
|
+
changes_count: 1
|
|
117
|
+
});
|
|
118
|
+
await expect(fs.readFile(path.join(tempDir, "workspace.txt"), "utf-8")).resolves.toBe(
|
|
119
|
+
"hello workspace"
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
it("normalizes workspace-contained absolute paths before editing", async () => {
|
|
123
|
+
const readText = vi.fn(async () => "hello world");
|
|
124
|
+
const writeFile = vi.fn(async () => void 0);
|
|
125
|
+
const workspaceManager = {
|
|
126
|
+
open: vi.fn(async () => ({
|
|
127
|
+
context: {
|
|
128
|
+
id: "test-workspace",
|
|
129
|
+
path: tempDir,
|
|
130
|
+
createdAt: Date.now(),
|
|
131
|
+
lastActiveAt: Date.now()
|
|
132
|
+
},
|
|
133
|
+
capabilities: ["files"],
|
|
134
|
+
files: {
|
|
135
|
+
readFile: readText,
|
|
136
|
+
readText,
|
|
137
|
+
glob: vi.fn(async () => []),
|
|
138
|
+
writeFile,
|
|
139
|
+
listFiles: vi.fn(async () => [])
|
|
140
|
+
}
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
const tool = createEditFileTool(vi.fn());
|
|
144
|
+
await tool.execute(
|
|
145
|
+
tool.inputSchema.parse({
|
|
146
|
+
file_path: path.join(tempDir, "workspace.txt"),
|
|
147
|
+
old_string: "world",
|
|
148
|
+
new_string: "workspace"
|
|
149
|
+
}),
|
|
150
|
+
createToolContext(mockLogger, {
|
|
151
|
+
services: {
|
|
152
|
+
...createWorkspaceServices(tempDir),
|
|
153
|
+
workspaceManager
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
expect(readText).toHaveBeenCalledWith("workspace.txt");
|
|
158
|
+
expect(writeFile).toHaveBeenCalledWith("workspace.txt", "hello workspace");
|
|
159
|
+
});
|
|
160
|
+
it("rejects external absolute paths before file provider calls", async () => {
|
|
161
|
+
const readText = vi.fn(async () => "hello world");
|
|
162
|
+
const writeFile = vi.fn(async () => void 0);
|
|
163
|
+
const workspaceManager = {
|
|
164
|
+
open: vi.fn(async () => ({
|
|
165
|
+
context: {
|
|
166
|
+
id: "test-workspace",
|
|
167
|
+
path: tempDir,
|
|
168
|
+
createdAt: Date.now(),
|
|
169
|
+
lastActiveAt: Date.now()
|
|
170
|
+
},
|
|
171
|
+
capabilities: ["files"],
|
|
172
|
+
files: {
|
|
173
|
+
readFile: readText,
|
|
174
|
+
readText,
|
|
175
|
+
glob: vi.fn(async () => []),
|
|
176
|
+
writeFile,
|
|
177
|
+
listFiles: vi.fn(async () => [])
|
|
178
|
+
}
|
|
179
|
+
}))
|
|
180
|
+
};
|
|
181
|
+
const tool = createEditFileTool(vi.fn());
|
|
182
|
+
await expect(
|
|
183
|
+
tool.execute(
|
|
184
|
+
tool.inputSchema.parse({
|
|
185
|
+
file_path: "/outside/workspace.txt",
|
|
186
|
+
old_string: "world",
|
|
187
|
+
new_string: "workspace"
|
|
188
|
+
}),
|
|
189
|
+
createToolContext(mockLogger, {
|
|
190
|
+
services: {
|
|
191
|
+
...createWorkspaceServices(tempDir),
|
|
192
|
+
workspaceManager
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
).rejects.toMatchObject({ code: ToolErrorCode.VALIDATION_FAILED });
|
|
197
|
+
expect(readText).not.toHaveBeenCalled();
|
|
198
|
+
expect(writeFile).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
it("should describe edit calls with an Edit header", () => {
|
|
201
|
+
const tool = createEditFileTool(async () => fileSystemService);
|
|
202
|
+
const header = tool.presentation?.describeHeader?.(
|
|
203
|
+
tool.inputSchema.parse({
|
|
204
|
+
file_path: "snake.py",
|
|
205
|
+
old_string: "old",
|
|
206
|
+
new_string: "new"
|
|
207
|
+
}),
|
|
208
|
+
createToolContext(mockLogger)
|
|
209
|
+
);
|
|
210
|
+
expect(header).toEqual({
|
|
211
|
+
argsText: "snake.py",
|
|
212
|
+
title: "Edit"
|
|
213
|
+
});
|
|
214
|
+
});
|
|
58
215
|
describe("File Modification Detection", () => {
|
|
59
216
|
it("should generate preview for files outside config-allowed roots (preview read only)", async () => {
|
|
60
217
|
const tool = createEditFileTool(async () => fileSystemService);
|