@dexto/tools-filesystem 1.5.2 → 1.5.4
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.integration.test.cjs +36 -32
- package/dist/directory-approval.integration.test.js +36 -32
- package/dist/edit-file-tool.cjs +43 -19
- package/dist/edit-file-tool.js +43 -19
- package/dist/edit-file-tool.test.cjs +203 -0
- package/dist/edit-file-tool.test.d.cts +2 -0
- package/dist/edit-file-tool.test.d.ts +2 -0
- package/dist/edit-file-tool.test.js +180 -0
- package/dist/filesystem-service.cjs +24 -14
- package/dist/filesystem-service.d.cts +8 -3
- package/dist/filesystem-service.d.ts +8 -3
- package/dist/filesystem-service.js +24 -14
- package/dist/filesystem-service.test.cjs +233 -0
- package/dist/filesystem-service.test.d.cts +2 -0
- package/dist/filesystem-service.test.d.ts +2 -0
- package/dist/filesystem-service.test.js +210 -0
- package/dist/glob-files-tool.cjs +56 -3
- package/dist/glob-files-tool.d.cts +4 -3
- package/dist/glob-files-tool.d.ts +4 -3
- package/dist/glob-files-tool.js +46 -3
- package/dist/grep-content-tool.cjs +55 -3
- package/dist/grep-content-tool.d.cts +4 -3
- package/dist/grep-content-tool.d.ts +4 -3
- package/dist/grep-content-tool.js +45 -3
- package/dist/path-validator.cjs +29 -20
- package/dist/path-validator.d.cts +9 -2
- package/dist/path-validator.d.ts +9 -2
- package/dist/path-validator.js +29 -20
- package/dist/path-validator.test.cjs +54 -48
- package/dist/path-validator.test.js +54 -48
- package/dist/read-file-tool.cjs +2 -2
- package/dist/read-file-tool.js +2 -2
- package/dist/tool-provider.cjs +22 -7
- package/dist/tool-provider.d.cts +4 -1
- package/dist/tool-provider.d.ts +4 -1
- package/dist/tool-provider.js +22 -7
- package/dist/types.d.cts +6 -0
- package/dist/types.d.ts +6 -0
- package/dist/write-file-tool.cjs +41 -7
- package/dist/write-file-tool.js +46 -8
- package/dist/write-file-tool.test.cjs +217 -0
- package/dist/write-file-tool.test.d.cts +2 -0
- package/dist/write-file-tool.test.d.ts +2 -0
- package/dist/write-file-tool.test.js +194 -0
- package/package.json +2 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { createWriteFileTool } from "./write-file-tool.js";
|
|
6
|
+
import { FileSystemService } from "./filesystem-service.js";
|
|
7
|
+
import { ToolErrorCode } from "@dexto/core";
|
|
8
|
+
import { DextoRuntimeError } from "@dexto/core";
|
|
9
|
+
const createMockLogger = () => ({
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
createChild: vi.fn().mockReturnThis()
|
|
15
|
+
});
|
|
16
|
+
describe("write_file tool", () => {
|
|
17
|
+
let mockLogger;
|
|
18
|
+
let tempDir;
|
|
19
|
+
let fileSystemService;
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
mockLogger = createMockLogger();
|
|
22
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-write-test-"));
|
|
23
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
24
|
+
fileSystemService = new FileSystemService(
|
|
25
|
+
{
|
|
26
|
+
allowedPaths: [tempDir],
|
|
27
|
+
blockedPaths: [],
|
|
28
|
+
blockedExtensions: [],
|
|
29
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
30
|
+
workingDirectory: tempDir,
|
|
31
|
+
enableBackups: false,
|
|
32
|
+
backupRetentionDays: 7
|
|
33
|
+
},
|
|
34
|
+
mockLogger
|
|
35
|
+
);
|
|
36
|
+
await fileSystemService.initialize();
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
describe("File Modification Detection - Existing Files", () => {
|
|
46
|
+
it("should succeed when existing file is not modified between preview and execute", async () => {
|
|
47
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
48
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
49
|
+
await fs.writeFile(testFile, "original content");
|
|
50
|
+
const toolCallId = "test-call-123";
|
|
51
|
+
const input = {
|
|
52
|
+
file_path: testFile,
|
|
53
|
+
content: "new content"
|
|
54
|
+
};
|
|
55
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
56
|
+
expect(preview).toBeDefined();
|
|
57
|
+
expect(preview?.type).toBe("diff");
|
|
58
|
+
const result = await tool.execute(input, { toolCallId });
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.path).toBe(testFile);
|
|
61
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
62
|
+
expect(content).toBe("new content");
|
|
63
|
+
});
|
|
64
|
+
it("should fail when existing file is modified between preview and execute", async () => {
|
|
65
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
66
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
67
|
+
await fs.writeFile(testFile, "original content");
|
|
68
|
+
const toolCallId = "test-call-456";
|
|
69
|
+
const input = {
|
|
70
|
+
file_path: testFile,
|
|
71
|
+
content: "new content"
|
|
72
|
+
};
|
|
73
|
+
await tool.generatePreview(input, { toolCallId });
|
|
74
|
+
await fs.writeFile(testFile, "user modified this");
|
|
75
|
+
try {
|
|
76
|
+
await tool.execute(input, { toolCallId });
|
|
77
|
+
expect.fail("Should have thrown an error");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
80
|
+
expect(error.code).toBe(
|
|
81
|
+
ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
85
|
+
expect(content).toBe("user modified this");
|
|
86
|
+
});
|
|
87
|
+
it("should fail when existing file is deleted between preview and execute", async () => {
|
|
88
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
89
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
90
|
+
await fs.writeFile(testFile, "original content");
|
|
91
|
+
const toolCallId = "test-call-deleted";
|
|
92
|
+
const input = {
|
|
93
|
+
file_path: testFile,
|
|
94
|
+
content: "new content"
|
|
95
|
+
};
|
|
96
|
+
await tool.generatePreview(input, { toolCallId });
|
|
97
|
+
await fs.unlink(testFile);
|
|
98
|
+
try {
|
|
99
|
+
await tool.execute(input, { toolCallId });
|
|
100
|
+
expect.fail("Should have thrown an error");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
103
|
+
expect(error.code).toBe(
|
|
104
|
+
ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("File Modification Detection - New Files", () => {
|
|
110
|
+
it("should succeed when creating new file that still does not exist", async () => {
|
|
111
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
112
|
+
const testFile = path.join(tempDir, "new-file.txt");
|
|
113
|
+
const toolCallId = "test-call-new";
|
|
114
|
+
const input = {
|
|
115
|
+
file_path: testFile,
|
|
116
|
+
content: "brand new content"
|
|
117
|
+
};
|
|
118
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
119
|
+
expect(preview).toBeDefined();
|
|
120
|
+
expect(preview?.type).toBe("file");
|
|
121
|
+
expect(preview.operation).toBe("create");
|
|
122
|
+
const result = await tool.execute(input, { toolCallId });
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
125
|
+
expect(content).toBe("brand new content");
|
|
126
|
+
});
|
|
127
|
+
it("should fail when file is created by someone else between preview and execute", async () => {
|
|
128
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
129
|
+
const testFile = path.join(tempDir, "race-condition.txt");
|
|
130
|
+
const toolCallId = "test-call-race";
|
|
131
|
+
const input = {
|
|
132
|
+
file_path: testFile,
|
|
133
|
+
content: "agent content"
|
|
134
|
+
};
|
|
135
|
+
const preview = await tool.generatePreview(input, { toolCallId });
|
|
136
|
+
expect(preview?.type).toBe("file");
|
|
137
|
+
await fs.writeFile(testFile, "someone else created this");
|
|
138
|
+
try {
|
|
139
|
+
await tool.execute(input, { toolCallId });
|
|
140
|
+
expect.fail("Should have thrown an error");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
143
|
+
expect(error.code).toBe(
|
|
144
|
+
ToolErrorCode.FILE_MODIFIED_SINCE_PREVIEW
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
148
|
+
expect(content).toBe("someone else created this");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("Cache Cleanup", () => {
|
|
152
|
+
it("should clean up hash cache after successful execution", async () => {
|
|
153
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
154
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
155
|
+
await fs.writeFile(testFile, "original");
|
|
156
|
+
const toolCallId = "test-call-cleanup";
|
|
157
|
+
const input = {
|
|
158
|
+
file_path: testFile,
|
|
159
|
+
content: "first write"
|
|
160
|
+
};
|
|
161
|
+
await tool.generatePreview(input, { toolCallId });
|
|
162
|
+
await tool.execute(input, { toolCallId });
|
|
163
|
+
const input2 = {
|
|
164
|
+
file_path: testFile,
|
|
165
|
+
content: "second write"
|
|
166
|
+
};
|
|
167
|
+
await tool.generatePreview(input2, { toolCallId });
|
|
168
|
+
const result = await tool.execute(input2, { toolCallId });
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
171
|
+
expect(content).toBe("second write");
|
|
172
|
+
});
|
|
173
|
+
it("should clean up hash cache after failed execution", async () => {
|
|
174
|
+
const tool = createWriteFileTool({ fileSystemService });
|
|
175
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
176
|
+
await fs.writeFile(testFile, "original");
|
|
177
|
+
const toolCallId = "test-call-fail";
|
|
178
|
+
const input = {
|
|
179
|
+
file_path: testFile,
|
|
180
|
+
content: "new content"
|
|
181
|
+
};
|
|
182
|
+
await tool.generatePreview(input, { toolCallId });
|
|
183
|
+
await fs.writeFile(testFile, "modified");
|
|
184
|
+
try {
|
|
185
|
+
await tool.execute(input, { toolCallId });
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
await fs.writeFile(testFile, "reset content");
|
|
189
|
+
await tool.generatePreview(input, { toolCallId });
|
|
190
|
+
const result = await tool.execute(input, { toolCallId });
|
|
191
|
+
expect(result.success).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/tools-filesystem",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "FileSystem tools provider for Dexto agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"glob": "^11.1.0",
|
|
23
23
|
"safe-regex": "^2.1.1",
|
|
24
24
|
"zod": "^3.25.0",
|
|
25
|
-
"@dexto/core": "1.5.
|
|
25
|
+
"@dexto/core": "1.5.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/diff": "^5.2.3",
|