@dexto/tools-filesystem 1.5.8 → 1.6.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/dist/directory-approval.cjs +98 -0
- package/dist/directory-approval.d.ts +24 -0
- package/dist/directory-approval.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.cjs +175 -390
- package/dist/directory-approval.integration.test.d.ts +14 -2
- package/dist/directory-approval.integration.test.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.js +178 -390
- package/dist/directory-approval.js +63 -0
- package/dist/edit-file-tool.cjs +109 -120
- package/dist/edit-file-tool.d.ts +22 -9
- package/dist/edit-file-tool.d.ts.map +1 -0
- package/dist/edit-file-tool.js +116 -110
- package/dist/edit-file-tool.test.cjs +109 -29
- package/dist/edit-file-tool.test.d.ts +7 -2
- package/dist/edit-file-tool.test.d.ts.map +1 -0
- package/dist/edit-file-tool.test.js +109 -29
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +6 -3
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +20 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/file-tool-types.d.ts +8 -40
- package/dist/file-tool-types.d.ts.map +1 -0
- package/dist/filesystem-service.cjs +325 -10
- package/dist/filesystem-service.d.ts +41 -12
- package/dist/filesystem-service.d.ts.map +1 -0
- package/dist/filesystem-service.js +326 -11
- package/dist/filesystem-service.test.cjs +10 -2
- package/dist/filesystem-service.test.d.ts +7 -2
- package/dist/filesystem-service.test.d.ts.map +1 -0
- package/dist/filesystem-service.test.js +10 -2
- package/dist/glob-files-tool.cjs +32 -46
- package/dist/glob-files-tool.d.ts +19 -9
- package/dist/glob-files-tool.d.ts.map +1 -0
- package/dist/glob-files-tool.js +33 -47
- package/dist/grep-content-tool.cjs +40 -45
- package/dist/grep-content-tool.d.ts +28 -9
- package/dist/grep-content-tool.d.ts.map +1 -0
- package/dist/grep-content-tool.js +41 -46
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +852 -14
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -2
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +20 -9
- package/dist/path-validator.d.ts.map +1 -0
- package/dist/path-validator.js +28 -2
- package/dist/path-validator.test.d.ts +7 -2
- package/dist/path-validator.test.d.ts.map +1 -0
- package/dist/read-file-tool.cjs +26 -59
- package/dist/read-file-tool.d.ts +19 -9
- package/dist/read-file-tool.d.ts.map +1 -0
- package/dist/read-file-tool.js +27 -50
- package/dist/tool-factory-config.cjs +61 -0
- package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +36 -0
- package/dist/tool-factory.cjs +123 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +102 -0
- package/dist/types.d.ts +82 -18
- package/dist/types.d.ts.map +1 -0
- package/dist/write-file-tool.cjs +93 -99
- package/dist/write-file-tool.d.ts +22 -9
- package/dist/write-file-tool.d.ts.map +1 -0
- package/dist/write-file-tool.js +97 -91
- package/dist/write-file-tool.test.cjs +139 -33
- package/dist/write-file-tool.test.d.ts +7 -2
- package/dist/write-file-tool.test.d.ts.map +1 -0
- package/dist/write-file-tool.test.js +139 -33
- package/package.json +5 -4
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -17
- package/dist/edit-file-tool.test.d.cts +0 -2
- package/dist/error-codes.d.cts +0 -32
- package/dist/errors.d.cts +0 -112
- package/dist/file-tool-types.d.cts +0 -46
- package/dist/filesystem-service.d.cts +0 -112
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -17
- package/dist/grep-content-tool.d.cts +0 -17
- package/dist/path-validator.d.cts +0 -97
- package/dist/path-validator.test.d.cts +0 -2
- package/dist/read-file-tool.d.cts +0 -17
- package/dist/tool-provider.cjs +0 -123
- package/dist/tool-provider.d.cts +0 -77
- package/dist/tool-provider.js +0 -99
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -17
- package/dist/write-file-tool.test.d.cts +0 -2
package/dist/edit-file-tool.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import { createPatch } from "diff";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
createLocalToolCallHeader,
|
|
6
|
+
DextoRuntimeError,
|
|
7
|
+
ToolError,
|
|
8
|
+
ToolErrorCode,
|
|
9
|
+
defineTool,
|
|
10
|
+
truncateForHeader
|
|
11
|
+
} from "@dexto/core";
|
|
9
12
|
import { FileSystemErrorCode } from "./error-codes.js";
|
|
13
|
+
import { createDirectoryAccessApprovalHandlers, resolveFilePath } from "./directory-approval.js";
|
|
10
14
|
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
11
15
|
function computeContentHash(content) {
|
|
12
16
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
13
17
|
}
|
|
14
18
|
const EditFileInputSchema = z.object({
|
|
15
|
-
file_path: z.string().describe("Absolute path to the file to edit"),
|
|
19
|
+
file_path: z.string().min(1).describe("Absolute path to the file to edit"),
|
|
16
20
|
old_string: z.string().describe("Text to replace (must be unique unless replace_all is true)"),
|
|
17
21
|
new_string: z.string().describe("Replacement text"),
|
|
18
22
|
replace_all: z.boolean().optional().default(false).describe("Replace all occurrences (default: false, requires unique match)")
|
|
@@ -25,134 +29,136 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
25
29
|
const deletions = (unified.match(/^-[^-]/gm) || []).length;
|
|
26
30
|
return {
|
|
27
31
|
type: "diff",
|
|
32
|
+
title: "Update file",
|
|
28
33
|
unified,
|
|
29
34
|
filename: filePath,
|
|
30
35
|
additions,
|
|
31
36
|
deletions
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
|
-
function createEditFileTool(
|
|
35
|
-
|
|
36
|
-
let pendingApprovalParentDir;
|
|
37
|
-
return {
|
|
39
|
+
function createEditFileTool(getFileSystemService) {
|
|
40
|
+
return defineTool({
|
|
38
41
|
id: "edit_file",
|
|
42
|
+
aliases: ["edit"],
|
|
39
43
|
description: "Edit a file by replacing text. By default, old_string must be unique in the file (will error if found multiple times). Set replace_all=true to replace all occurrences. Automatically creates backup before editing. Requires approval. Returns success status, path, number of changes made, and backup path.",
|
|
40
44
|
inputSchema: EditFileInputSchema,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const occurrences = originalContent.split(old_string).length - 1;
|
|
99
|
-
if (occurrences > 1) {
|
|
45
|
+
...createDirectoryAccessApprovalHandlers({
|
|
46
|
+
toolName: "edit_file",
|
|
47
|
+
operation: "edit",
|
|
48
|
+
inputSchema: EditFileInputSchema,
|
|
49
|
+
getFileSystemService,
|
|
50
|
+
resolvePaths: (input, fileSystemService) => resolveFilePath(fileSystemService.getWorkingDirectory(), input.file_path)
|
|
51
|
+
}),
|
|
52
|
+
presentation: {
|
|
53
|
+
describeHeader: (input) => createLocalToolCallHeader({
|
|
54
|
+
title: "Update",
|
|
55
|
+
argsText: truncateForHeader(input.file_path, 140)
|
|
56
|
+
}),
|
|
57
|
+
/**
|
|
58
|
+
* Generate preview for approval UI - shows diff without modifying file
|
|
59
|
+
* Throws ToolError.validationFailed() for validation errors (file not found, string not found)
|
|
60
|
+
* Stores content hash for change detection in execute phase.
|
|
61
|
+
*/
|
|
62
|
+
preview: async (input, context) => {
|
|
63
|
+
const { file_path, old_string, new_string, replace_all } = input;
|
|
64
|
+
const resolvedFileSystemService = await getFileSystemService(context);
|
|
65
|
+
const { path: resolvedPath } = resolveFilePath(
|
|
66
|
+
resolvedFileSystemService.getWorkingDirectory(),
|
|
67
|
+
file_path
|
|
68
|
+
);
|
|
69
|
+
try {
|
|
70
|
+
let originalContent;
|
|
71
|
+
try {
|
|
72
|
+
const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
73
|
+
originalContent = originalFile.content;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.INVALID_PATH) {
|
|
76
|
+
const originalFile = await resolvedFileSystemService.readFileForToolPreview(
|
|
77
|
+
resolvedPath
|
|
78
|
+
);
|
|
79
|
+
originalContent = originalFile.content;
|
|
80
|
+
} else {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (context.toolCallId) {
|
|
85
|
+
previewContentHashCache.set(
|
|
86
|
+
context.toolCallId,
|
|
87
|
+
computeContentHash(originalContent)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!replace_all) {
|
|
91
|
+
const occurrences = originalContent.split(old_string).length - 1;
|
|
92
|
+
if (occurrences > 1) {
|
|
93
|
+
throw ToolError.validationFailed(
|
|
94
|
+
"edit_file",
|
|
95
|
+
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
96
|
+
{ file_path: resolvedPath, occurrences }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
101
|
+
if (originalContent === newContent) {
|
|
100
102
|
throw ToolError.validationFailed(
|
|
101
103
|
"edit_file",
|
|
102
|
-
`String found
|
|
103
|
-
{
|
|
104
|
+
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
105
|
+
{
|
|
106
|
+
file_path: resolvedPath,
|
|
107
|
+
old_string_preview: old_string.slice(0, 100)
|
|
108
|
+
}
|
|
104
109
|
);
|
|
105
110
|
}
|
|
111
|
+
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (error instanceof DextoRuntimeError) {
|
|
117
|
+
throw ToolError.validationFailed("edit_file", error.message, {
|
|
118
|
+
file_path: resolvedPath,
|
|
119
|
+
originalErrorCode: error.code
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
106
123
|
}
|
|
107
|
-
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
108
|
-
if (originalContent === newContent) {
|
|
109
|
-
throw ToolError.validationFailed(
|
|
110
|
-
"edit_file",
|
|
111
|
-
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
112
|
-
{ file_path, old_string_preview: old_string.slice(0, 100) }
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return generateDiffPreview(file_path, originalContent, newContent);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (error instanceof DextoRuntimeError && error.code === ToolErrorCode.VALIDATION_FAILED) {
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
if (error instanceof DextoRuntimeError) {
|
|
121
|
-
throw ToolError.validationFailed("edit_file", error.message, {
|
|
122
|
-
file_path,
|
|
123
|
-
originalErrorCode: error.code
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
124
|
}
|
|
128
125
|
},
|
|
129
|
-
|
|
126
|
+
async execute(input, context) {
|
|
127
|
+
const resolvedFileSystemService = await getFileSystemService(context);
|
|
130
128
|
const { file_path, old_string, new_string, replace_all } = input;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
129
|
+
const { path: resolvedPath } = resolveFilePath(
|
|
130
|
+
resolvedFileSystemService.getWorkingDirectory(),
|
|
131
|
+
file_path
|
|
132
|
+
);
|
|
133
|
+
const toolCallId = context.toolCallId;
|
|
134
|
+
if (toolCallId) {
|
|
135
|
+
const expectedHash = previewContentHashCache.get(toolCallId);
|
|
136
|
+
if (expectedHash === void 0) {
|
|
137
|
+
} else {
|
|
138
|
+
previewContentHashCache.delete(toolCallId);
|
|
139
|
+
let currentContent;
|
|
140
|
+
try {
|
|
141
|
+
const currentFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
142
|
+
currentContent = currentFile.content;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof DextoRuntimeError && error.code === FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
145
|
+
throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const currentHash = computeContentHash(currentContent);
|
|
150
|
+
if (expectedHash !== currentHash) {
|
|
151
|
+
throw ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
|
|
141
152
|
}
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
const currentHash = computeContentHash(currentContent);
|
|
145
|
-
if (expectedHash !== currentHash) {
|
|
146
|
-
throw ToolError.fileModifiedSincePreview("edit_file", file_path);
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
|
-
const result = await
|
|
155
|
+
const result = await resolvedFileSystemService.editFile(resolvedPath, {
|
|
150
156
|
oldString: old_string,
|
|
151
157
|
newString: new_string,
|
|
152
158
|
replaceAll: replace_all
|
|
153
159
|
});
|
|
154
160
|
const _display = generateDiffPreview(
|
|
155
|
-
|
|
161
|
+
resolvedPath,
|
|
156
162
|
result.originalContent,
|
|
157
163
|
result.newContent
|
|
158
164
|
);
|
|
@@ -164,7 +170,7 @@ function createEditFileTool(options) {
|
|
|
164
170
|
_display
|
|
165
171
|
};
|
|
166
172
|
}
|
|
167
|
-
};
|
|
173
|
+
});
|
|
168
174
|
}
|
|
169
175
|
export {
|
|
170
176
|
createEditFileTool
|
|
@@ -29,13 +29,26 @@ var import_edit_file_tool = require("./edit-file-tool.js");
|
|
|
29
29
|
var import_filesystem_service = require("./filesystem-service.js");
|
|
30
30
|
var import_core = require("@dexto/core");
|
|
31
31
|
var import_core2 = require("@dexto/core");
|
|
32
|
-
const createMockLogger = () =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const createMockLogger = () => {
|
|
33
|
+
const logger = {
|
|
34
|
+
debug: import_vitest.vi.fn(),
|
|
35
|
+
silly: import_vitest.vi.fn(),
|
|
36
|
+
info: import_vitest.vi.fn(),
|
|
37
|
+
warn: import_vitest.vi.fn(),
|
|
38
|
+
error: import_vitest.vi.fn(),
|
|
39
|
+
trackException: import_vitest.vi.fn(),
|
|
40
|
+
createChild: import_vitest.vi.fn(() => logger),
|
|
41
|
+
createFileOnlyChild: import_vitest.vi.fn(() => logger),
|
|
42
|
+
setLevel: import_vitest.vi.fn(),
|
|
43
|
+
getLevel: import_vitest.vi.fn(() => "debug"),
|
|
44
|
+
getLogFilePath: import_vitest.vi.fn(() => null),
|
|
45
|
+
destroy: import_vitest.vi.fn(async () => void 0)
|
|
46
|
+
};
|
|
47
|
+
return logger;
|
|
48
|
+
};
|
|
49
|
+
function createToolContext(logger, overrides = {}) {
|
|
50
|
+
return { logger, ...overrides };
|
|
51
|
+
}
|
|
39
52
|
(0, import_vitest.describe)("edit_file tool", () => {
|
|
40
53
|
let mockLogger;
|
|
41
54
|
let tempDir;
|
|
@@ -66,8 +79,43 @@ const createMockLogger = () => ({
|
|
|
66
79
|
}
|
|
67
80
|
});
|
|
68
81
|
(0, import_vitest.describe)("File Modification Detection", () => {
|
|
82
|
+
(0, import_vitest.it)("should generate preview for files outside config-allowed roots (preview read only)", async () => {
|
|
83
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
84
|
+
const previewFn = tool.presentation?.preview;
|
|
85
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
86
|
+
const rawExternalDir = await fs.mkdtemp(
|
|
87
|
+
path.join(os.tmpdir(), "dexto-edit-outside-allowed-")
|
|
88
|
+
);
|
|
89
|
+
const externalDir = await fs.realpath(rawExternalDir);
|
|
90
|
+
const externalFile = path.join(externalDir, "external.txt");
|
|
91
|
+
try {
|
|
92
|
+
await fs.writeFile(externalFile, "hello world");
|
|
93
|
+
const toolCallId = "preview-outside-roots";
|
|
94
|
+
const parsedInput = tool.inputSchema.parse({
|
|
95
|
+
file_path: externalFile,
|
|
96
|
+
old_string: "world",
|
|
97
|
+
new_string: "universe"
|
|
98
|
+
});
|
|
99
|
+
const preview = await previewFn(
|
|
100
|
+
parsedInput,
|
|
101
|
+
createToolContext(mockLogger, { toolCallId })
|
|
102
|
+
);
|
|
103
|
+
(0, import_vitest.expect)(preview).toBeDefined();
|
|
104
|
+
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
105
|
+
if (preview?.type === "diff") {
|
|
106
|
+
(0, import_vitest.expect)(preview.title).toBe("Update file");
|
|
107
|
+
(0, import_vitest.expect)(preview.filename).toBe(externalFile);
|
|
108
|
+
} else {
|
|
109
|
+
import_vitest.expect.fail("Expected diff preview");
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
await fs.rm(externalDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
69
115
|
(0, import_vitest.it)("should succeed when file is not modified between preview and execute", async () => {
|
|
70
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
116
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
117
|
+
const previewFn = tool.presentation?.preview;
|
|
118
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
71
119
|
const testFile = path.join(tempDir, "test.txt");
|
|
72
120
|
await fs.writeFile(testFile, "hello world");
|
|
73
121
|
const toolCallId = "test-call-123";
|
|
@@ -76,16 +124,25 @@ const createMockLogger = () => ({
|
|
|
76
124
|
old_string: "world",
|
|
77
125
|
new_string: "universe"
|
|
78
126
|
};
|
|
79
|
-
const
|
|
127
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
128
|
+
const preview = await previewFn(
|
|
129
|
+
parsedInput,
|
|
130
|
+
createToolContext(mockLogger, { toolCallId })
|
|
131
|
+
);
|
|
80
132
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
81
|
-
const result = await tool.execute(
|
|
133
|
+
const result = await tool.execute(
|
|
134
|
+
parsedInput,
|
|
135
|
+
createToolContext(mockLogger, { toolCallId })
|
|
136
|
+
);
|
|
82
137
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
83
138
|
(0, import_vitest.expect)(result.path).toBe(testFile);
|
|
84
139
|
const content = await fs.readFile(testFile, "utf-8");
|
|
85
140
|
(0, import_vitest.expect)(content).toBe("hello universe");
|
|
86
141
|
});
|
|
87
142
|
(0, import_vitest.it)("should fail when file is modified between preview and execute", async () => {
|
|
88
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
143
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
144
|
+
const previewFn = tool.presentation?.preview;
|
|
145
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
89
146
|
const testFile = path.join(tempDir, "test.txt");
|
|
90
147
|
await fs.writeFile(testFile, "hello world");
|
|
91
148
|
const toolCallId = "test-call-456";
|
|
@@ -94,11 +151,15 @@ const createMockLogger = () => ({
|
|
|
94
151
|
old_string: "world",
|
|
95
152
|
new_string: "universe"
|
|
96
153
|
};
|
|
97
|
-
const
|
|
154
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
155
|
+
const preview = await previewFn(
|
|
156
|
+
parsedInput,
|
|
157
|
+
createToolContext(mockLogger, { toolCallId })
|
|
158
|
+
);
|
|
98
159
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
99
160
|
await fs.writeFile(testFile, "hello world - user added this");
|
|
100
161
|
try {
|
|
101
|
-
await tool.execute(
|
|
162
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
102
163
|
import_vitest.expect.fail("Should have thrown an error");
|
|
103
164
|
} catch (error) {
|
|
104
165
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
@@ -110,7 +171,9 @@ const createMockLogger = () => ({
|
|
|
110
171
|
(0, import_vitest.expect)(content).toBe("hello world - user added this");
|
|
111
172
|
});
|
|
112
173
|
(0, import_vitest.it)("should detect file modification with correct error code", async () => {
|
|
113
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
174
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
175
|
+
const previewFn = tool.presentation?.preview;
|
|
176
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
114
177
|
const testFile = path.join(tempDir, "test.txt");
|
|
115
178
|
await fs.writeFile(testFile, "hello world");
|
|
116
179
|
const toolCallId = "test-call-789";
|
|
@@ -119,10 +182,11 @@ const createMockLogger = () => ({
|
|
|
119
182
|
old_string: "world",
|
|
120
183
|
new_string: "universe"
|
|
121
184
|
};
|
|
122
|
-
|
|
185
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
186
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
123
187
|
await fs.writeFile(testFile, "hello world modified");
|
|
124
188
|
try {
|
|
125
|
-
await tool.execute(
|
|
189
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
126
190
|
import_vitest.expect.fail("Should have thrown an error");
|
|
127
191
|
} catch (error) {
|
|
128
192
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
@@ -136,7 +200,9 @@ const createMockLogger = () => ({
|
|
|
136
200
|
}
|
|
137
201
|
});
|
|
138
202
|
(0, import_vitest.it)("should work without toolCallId (no modification check)", async () => {
|
|
139
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
203
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
204
|
+
const previewFn = tool.presentation?.preview;
|
|
205
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
140
206
|
const testFile = path.join(tempDir, "test.txt");
|
|
141
207
|
await fs.writeFile(testFile, "hello world");
|
|
142
208
|
const input = {
|
|
@@ -144,10 +210,11 @@ const createMockLogger = () => ({
|
|
|
144
210
|
old_string: "world",
|
|
145
211
|
new_string: "universe"
|
|
146
212
|
};
|
|
147
|
-
|
|
213
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
214
|
+
await previewFn(parsedInput, createToolContext(mockLogger));
|
|
148
215
|
await fs.writeFile(testFile, "hello world changed");
|
|
149
216
|
try {
|
|
150
|
-
await tool.execute(
|
|
217
|
+
await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
151
218
|
} catch (error) {
|
|
152
219
|
(0, import_vitest.expect)(error).toBeInstanceOf(import_core2.DextoRuntimeError);
|
|
153
220
|
(0, import_vitest.expect)(error.code).not.toBe(
|
|
@@ -156,7 +223,9 @@ const createMockLogger = () => ({
|
|
|
156
223
|
}
|
|
157
224
|
});
|
|
158
225
|
(0, import_vitest.it)("should clean up hash cache after successful execution", async () => {
|
|
159
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
226
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
227
|
+
const previewFn = tool.presentation?.preview;
|
|
228
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
160
229
|
const testFile = path.join(tempDir, "test.txt");
|
|
161
230
|
await fs.writeFile(testFile, "hello world");
|
|
162
231
|
const toolCallId = "test-call-cleanup";
|
|
@@ -165,21 +234,28 @@ const createMockLogger = () => ({
|
|
|
165
234
|
old_string: "world",
|
|
166
235
|
new_string: "universe"
|
|
167
236
|
};
|
|
168
|
-
|
|
169
|
-
await
|
|
237
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
238
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
239
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
170
240
|
const input2 = {
|
|
171
241
|
file_path: testFile,
|
|
172
242
|
old_string: "universe",
|
|
173
243
|
new_string: "galaxy"
|
|
174
244
|
};
|
|
175
|
-
|
|
176
|
-
|
|
245
|
+
const parsedInput2 = tool.inputSchema.parse(input2);
|
|
246
|
+
await previewFn(parsedInput2, createToolContext(mockLogger, { toolCallId }));
|
|
247
|
+
const result = await tool.execute(
|
|
248
|
+
parsedInput2,
|
|
249
|
+
createToolContext(mockLogger, { toolCallId })
|
|
250
|
+
);
|
|
177
251
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
178
252
|
const content = await fs.readFile(testFile, "utf-8");
|
|
179
253
|
(0, import_vitest.expect)(content).toBe("hello galaxy");
|
|
180
254
|
});
|
|
181
255
|
(0, import_vitest.it)("should clean up hash cache after failed execution", async () => {
|
|
182
|
-
const tool = (0, import_edit_file_tool.createEditFileTool)(
|
|
256
|
+
const tool = (0, import_edit_file_tool.createEditFileTool)(async () => fileSystemService);
|
|
257
|
+
const previewFn = tool.presentation?.preview;
|
|
258
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
183
259
|
const testFile = path.join(tempDir, "test.txt");
|
|
184
260
|
await fs.writeFile(testFile, "hello world");
|
|
185
261
|
const toolCallId = "test-call-fail-cleanup";
|
|
@@ -188,15 +264,19 @@ const createMockLogger = () => ({
|
|
|
188
264
|
old_string: "world",
|
|
189
265
|
new_string: "universe"
|
|
190
266
|
};
|
|
191
|
-
|
|
267
|
+
const parsedInput = tool.inputSchema.parse(input);
|
|
268
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
192
269
|
await fs.writeFile(testFile, "hello world modified");
|
|
193
270
|
try {
|
|
194
|
-
await tool.execute(
|
|
271
|
+
await tool.execute(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
195
272
|
} catch {
|
|
196
273
|
}
|
|
197
274
|
await fs.writeFile(testFile, "hello world");
|
|
198
|
-
await
|
|
199
|
-
const result = await tool.execute(
|
|
275
|
+
await previewFn(parsedInput, createToolContext(mockLogger, { toolCallId }));
|
|
276
|
+
const result = await tool.execute(
|
|
277
|
+
parsedInput,
|
|
278
|
+
createToolContext(mockLogger, { toolCallId })
|
|
279
|
+
);
|
|
200
280
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
201
281
|
});
|
|
202
282
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-file-tool.test.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|