@dexto/tools-filesystem 1.5.8 → 1.6.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.
Files changed (83) hide show
  1. package/dist/directory-approval.cjs +94 -0
  2. package/dist/directory-approval.d.cts +22 -0
  3. package/dist/directory-approval.d.ts +20 -0
  4. package/dist/directory-approval.d.ts.map +1 -0
  5. package/dist/directory-approval.integration.test.cjs +303 -269
  6. package/dist/directory-approval.integration.test.d.ts +14 -2
  7. package/dist/directory-approval.integration.test.d.ts.map +1 -0
  8. package/dist/directory-approval.integration.test.js +309 -270
  9. package/dist/directory-approval.js +59 -0
  10. package/dist/edit-file-tool.cjs +57 -90
  11. package/dist/edit-file-tool.d.cts +20 -3
  12. package/dist/edit-file-tool.d.ts +22 -9
  13. package/dist/edit-file-tool.d.ts.map +1 -0
  14. package/dist/edit-file-tool.js +53 -76
  15. package/dist/edit-file-tool.test.cjs +66 -29
  16. package/dist/edit-file-tool.test.d.ts +7 -2
  17. package/dist/edit-file-tool.test.d.ts.map +1 -0
  18. package/dist/edit-file-tool.test.js +66 -29
  19. package/dist/error-codes.d.ts +2 -3
  20. package/dist/error-codes.d.ts.map +1 -0
  21. package/dist/errors.d.ts +4 -7
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/file-tool-types.d.cts +7 -35
  24. package/dist/file-tool-types.d.ts +8 -40
  25. package/dist/file-tool-types.d.ts.map +1 -0
  26. package/dist/filesystem-service.cjs +18 -1
  27. package/dist/filesystem-service.d.cts +11 -6
  28. package/dist/filesystem-service.d.ts +14 -12
  29. package/dist/filesystem-service.d.ts.map +1 -0
  30. package/dist/filesystem-service.js +18 -1
  31. package/dist/filesystem-service.test.cjs +10 -2
  32. package/dist/filesystem-service.test.d.ts +7 -2
  33. package/dist/filesystem-service.test.d.ts.map +1 -0
  34. package/dist/filesystem-service.test.js +10 -2
  35. package/dist/glob-files-tool.cjs +22 -47
  36. package/dist/glob-files-tool.d.cts +17 -3
  37. package/dist/glob-files-tool.d.ts +19 -9
  38. package/dist/glob-files-tool.d.ts.map +1 -0
  39. package/dist/glob-files-tool.js +23 -48
  40. package/dist/grep-content-tool.cjs +29 -46
  41. package/dist/grep-content-tool.d.cts +26 -3
  42. package/dist/grep-content-tool.d.ts +28 -9
  43. package/dist/grep-content-tool.d.ts.map +1 -0
  44. package/dist/grep-content-tool.js +30 -47
  45. package/dist/index.cjs +3 -3
  46. package/dist/index.d.cts +4 -2
  47. package/dist/index.d.ts +10 -5
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +2 -2
  50. package/dist/path-validator.d.cts +2 -2
  51. package/dist/path-validator.d.ts +6 -9
  52. package/dist/path-validator.d.ts.map +1 -0
  53. package/dist/path-validator.test.d.ts +7 -2
  54. package/dist/path-validator.test.d.ts.map +1 -0
  55. package/dist/read-file-tool.cjs +21 -60
  56. package/dist/read-file-tool.d.cts +17 -3
  57. package/dist/read-file-tool.d.ts +19 -9
  58. package/dist/read-file-tool.d.ts.map +1 -0
  59. package/dist/read-file-tool.js +22 -51
  60. package/dist/tool-factory-config.cjs +61 -0
  61. package/dist/{tool-provider.d.ts → tool-factory-config.d.cts} +9 -23
  62. package/dist/{tool-provider.d.cts → tool-factory-config.d.ts} +13 -30
  63. package/dist/tool-factory-config.d.ts.map +1 -0
  64. package/dist/tool-factory-config.js +36 -0
  65. package/dist/tool-factory.cjs +102 -0
  66. package/dist/tool-factory.d.cts +7 -0
  67. package/dist/tool-factory.d.ts +4 -0
  68. package/dist/tool-factory.d.ts.map +1 -0
  69. package/dist/tool-factory.js +81 -0
  70. package/dist/types.d.ts +17 -18
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/write-file-tool.cjs +45 -73
  73. package/dist/write-file-tool.d.cts +20 -3
  74. package/dist/write-file-tool.d.ts +22 -9
  75. package/dist/write-file-tool.d.ts.map +1 -0
  76. package/dist/write-file-tool.js +46 -68
  77. package/dist/write-file-tool.test.cjs +76 -32
  78. package/dist/write-file-tool.test.d.ts +7 -2
  79. package/dist/write-file-tool.test.d.ts.map +1 -0
  80. package/dist/write-file-tool.test.js +76 -32
  81. package/package.json +4 -3
  82. package/dist/tool-provider.cjs +0 -123
  83. package/dist/tool-provider.js +0 -99
@@ -6,21 +6,49 @@ import { createReadFileTool } from "./read-file-tool.js";
6
6
  import { createWriteFileTool } from "./write-file-tool.js";
7
7
  import { createEditFileTool } from "./edit-file-tool.js";
8
8
  import { FileSystemService } from "./filesystem-service.js";
9
- import { ApprovalType, ApprovalStatus } from "@dexto/core";
10
- const createMockLogger = () => ({
11
- debug: vi.fn(),
12
- info: vi.fn(),
13
- warn: vi.fn(),
14
- error: vi.fn(),
15
- createChild: vi.fn().mockReturnThis()
16
- });
9
+ import {
10
+ ApprovalManager,
11
+ ApprovalStatus,
12
+ ApprovalType,
13
+ DextoRuntimeError
14
+ } from "@dexto/core";
15
+ const createMockLogger = () => {
16
+ const noopAsync = async () => void 0;
17
+ const logger = {
18
+ debug: vi.fn(),
19
+ silly: vi.fn(),
20
+ info: vi.fn(),
21
+ warn: vi.fn(),
22
+ error: vi.fn(),
23
+ trackException: vi.fn(),
24
+ createChild: () => logger,
25
+ setLevel: vi.fn(),
26
+ getLevel: () => "info",
27
+ getLogFilePath: () => null,
28
+ destroy: noopAsync
29
+ };
30
+ return logger;
31
+ };
32
+ function createToolContext(logger, approval) {
33
+ return {
34
+ logger,
35
+ services: {
36
+ approval,
37
+ search: {},
38
+ resources: {},
39
+ prompts: {},
40
+ mcp: {},
41
+ taskForker: null
42
+ }
43
+ };
44
+ }
17
45
  describe("Directory Approval Integration Tests", () => {
18
46
  let mockLogger;
19
47
  let tempDir;
20
48
  let fileSystemService;
21
- let directoryApproval;
22
- let isSessionApprovedMock;
23
- let addApprovedMock;
49
+ let approvalManager;
50
+ let toolContext;
51
+ const getFileSystemService = async (_context) => fileSystemService;
24
52
  beforeEach(async () => {
25
53
  mockLogger = createMockLogger();
26
54
  const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
@@ -38,12 +66,14 @@ describe("Directory Approval Integration Tests", () => {
38
66
  mockLogger
39
67
  );
40
68
  await fileSystemService.initialize();
41
- isSessionApprovedMock = vi.fn().mockReturnValue(false);
42
- addApprovedMock = vi.fn();
43
- directoryApproval = {
44
- isSessionApproved: isSessionApprovedMock,
45
- addApproved: addApprovedMock
46
- };
69
+ approvalManager = new ApprovalManager(
70
+ {
71
+ permissions: { mode: "manual" },
72
+ elicitation: { enabled: true }
73
+ },
74
+ mockLogger
75
+ );
76
+ toolContext = createToolContext(mockLogger, approvalManager);
47
77
  vi.clearAllMocks();
48
78
  });
49
79
  afterEach(async () => {
@@ -55,22 +85,22 @@ describe("Directory Approval Integration Tests", () => {
55
85
  describe("Read File Tool", () => {
56
86
  describe("getApprovalOverride", () => {
57
87
  it("should return null for paths within working directory (no prompt needed)", async () => {
58
- const tool = createReadFileTool({
59
- fileSystemService,
60
- directoryApproval
61
- });
88
+ const tool = createReadFileTool(getFileSystemService);
62
89
  const testFile = path.join(tempDir, "test.txt");
63
90
  await fs.writeFile(testFile, "test content");
64
- const override = await tool.getApprovalOverride?.({ file_path: testFile });
91
+ const override = await tool.getApprovalOverride?.(
92
+ tool.inputSchema.parse({ file_path: testFile }),
93
+ toolContext
94
+ );
65
95
  expect(override).toBeNull();
66
96
  });
67
97
  it("should return directory access approval for external paths", async () => {
68
- const tool = createReadFileTool({
69
- fileSystemService,
70
- directoryApproval
71
- });
98
+ const tool = createReadFileTool(getFileSystemService);
72
99
  const externalPath = "/external/project/file.ts";
73
- const override = await tool.getApprovalOverride?.({ file_path: externalPath });
100
+ const override = await tool.getApprovalOverride?.(
101
+ tool.inputSchema.parse({ file_path: externalPath }),
102
+ toolContext
103
+ );
74
104
  expect(override).not.toBeNull();
75
105
  expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
76
106
  const metadata = override?.metadata;
@@ -80,101 +110,96 @@ describe("Directory Approval Integration Tests", () => {
80
110
  expect(metadata?.toolName).toBe("read_file");
81
111
  });
82
112
  it("should return null when external path is session-approved", async () => {
83
- isSessionApprovedMock.mockReturnValue(true);
84
- const tool = createReadFileTool({
85
- fileSystemService,
86
- directoryApproval
87
- });
113
+ approvalManager.addApprovedDirectory("/external/project", "session");
114
+ const tool = createReadFileTool(getFileSystemService);
88
115
  const externalPath = "/external/project/file.ts";
89
- const override = await tool.getApprovalOverride?.({ file_path: externalPath });
90
- expect(override).toBeNull();
91
- expect(isSessionApprovedMock).toHaveBeenCalledWith(externalPath);
92
- });
93
- it("should return null when file_path is missing", async () => {
94
- const tool = createReadFileTool({
95
- fileSystemService,
96
- directoryApproval
97
- });
98
- const override = await tool.getApprovalOverride?.({});
116
+ const override = await tool.getApprovalOverride?.(
117
+ tool.inputSchema.parse({ file_path: externalPath }),
118
+ toolContext
119
+ );
99
120
  expect(override).toBeNull();
100
121
  });
101
122
  });
102
123
  describe("onApprovalGranted", () => {
103
124
  it("should add directory as session-approved when rememberDirectory is true", async () => {
104
- const tool = createReadFileTool({
105
- fileSystemService,
106
- directoryApproval
107
- });
125
+ const tool = createReadFileTool(getFileSystemService);
108
126
  const externalPath = "/external/project/file.ts";
109
- await tool.getApprovalOverride?.({ file_path: externalPath });
110
- tool.onApprovalGranted?.({
111
- approvalId: "test-approval",
112
- status: ApprovalStatus.APPROVED,
113
- data: { rememberDirectory: true }
114
- });
115
- expect(addApprovedMock).toHaveBeenCalledWith(
116
- path.dirname(path.resolve(externalPath)),
117
- "session"
127
+ const approvalRequest = await tool.getApprovalOverride?.(
128
+ tool.inputSchema.parse({ file_path: externalPath }),
129
+ toolContext
130
+ );
131
+ expect(approvalRequest).not.toBeNull();
132
+ if (!approvalRequest) {
133
+ throw new Error("Expected approval request");
134
+ }
135
+ tool.onApprovalGranted?.(
136
+ {
137
+ approvalId: "test-approval",
138
+ status: ApprovalStatus.APPROVED,
139
+ data: { rememberDirectory: true }
140
+ },
141
+ toolContext,
142
+ approvalRequest
118
143
  );
144
+ expect(
145
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
146
+ ).toBe("session");
119
147
  });
120
148
  it("should add directory as once-approved when rememberDirectory is false", async () => {
121
- const tool = createReadFileTool({
122
- fileSystemService,
123
- directoryApproval
124
- });
149
+ const tool = createReadFileTool(getFileSystemService);
125
150
  const externalPath = "/external/project/file.ts";
126
- await tool.getApprovalOverride?.({ file_path: externalPath });
127
- tool.onApprovalGranted?.({
128
- approvalId: "test-approval",
129
- status: ApprovalStatus.APPROVED,
130
- data: { rememberDirectory: false }
131
- });
132
- expect(addApprovedMock).toHaveBeenCalledWith(
133
- path.dirname(path.resolve(externalPath)),
134
- "once"
151
+ const approvalRequest = await tool.getApprovalOverride?.(
152
+ tool.inputSchema.parse({ file_path: externalPath }),
153
+ toolContext
135
154
  );
155
+ expect(approvalRequest).not.toBeNull();
156
+ if (!approvalRequest) {
157
+ throw new Error("Expected approval request");
158
+ }
159
+ tool.onApprovalGranted?.(
160
+ {
161
+ approvalId: "test-approval",
162
+ status: ApprovalStatus.APPROVED,
163
+ data: { rememberDirectory: false }
164
+ },
165
+ toolContext,
166
+ approvalRequest
167
+ );
168
+ expect(
169
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
170
+ ).toBe("once");
136
171
  });
137
172
  it("should default to once-approved when rememberDirectory is not specified", async () => {
138
- const tool = createReadFileTool({
139
- fileSystemService,
140
- directoryApproval
141
- });
173
+ const tool = createReadFileTool(getFileSystemService);
142
174
  const externalPath = "/external/project/file.ts";
143
- await tool.getApprovalOverride?.({ file_path: externalPath });
144
- tool.onApprovalGranted?.({
145
- approvalId: "test-approval",
146
- status: ApprovalStatus.APPROVED,
147
- data: {}
148
- });
149
- expect(addApprovedMock).toHaveBeenCalledWith(
150
- path.dirname(path.resolve(externalPath)),
151
- "once"
175
+ const approvalRequest = await tool.getApprovalOverride?.(
176
+ tool.inputSchema.parse({ file_path: externalPath }),
177
+ toolContext
152
178
  );
153
- });
154
- it("should not call addApproved when directoryApproval is not provided", async () => {
155
- const tool = createReadFileTool({
156
- fileSystemService,
157
- directoryApproval: void 0
158
- });
159
- const externalPath = "/external/project/file.ts";
160
- await tool.getApprovalOverride?.({ file_path: externalPath });
161
- tool.onApprovalGranted?.({
162
- approvalId: "test-approval",
163
- status: ApprovalStatus.APPROVED,
164
- data: { rememberDirectory: true }
165
- });
166
- expect(addApprovedMock).not.toHaveBeenCalled();
179
+ expect(approvalRequest).not.toBeNull();
180
+ if (!approvalRequest) {
181
+ throw new Error("Expected approval request");
182
+ }
183
+ tool.onApprovalGranted?.(
184
+ {
185
+ approvalId: "test-approval",
186
+ status: ApprovalStatus.APPROVED,
187
+ data: {}
188
+ },
189
+ toolContext,
190
+ approvalRequest
191
+ );
192
+ expect(
193
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
194
+ ).toBe("once");
167
195
  });
168
196
  });
169
197
  describe("execute", () => {
170
198
  it("should read file contents within working directory", async () => {
171
- const tool = createReadFileTool({
172
- fileSystemService,
173
- directoryApproval
174
- });
199
+ const tool = createReadFileTool(getFileSystemService);
175
200
  const testFile = path.join(tempDir, "readable.txt");
176
201
  await fs.writeFile(testFile, "Hello, world!\nLine 2");
177
- const result = await tool.execute({ file_path: testFile }, {});
202
+ const result = await tool.execute({ file_path: testFile }, toolContext);
178
203
  expect(result.content).toBe("Hello, world!\nLine 2");
179
204
  expect(result.lines).toBe(2);
180
205
  });
@@ -183,27 +208,21 @@ describe("Directory Approval Integration Tests", () => {
183
208
  describe("Write File Tool", () => {
184
209
  describe("getApprovalOverride", () => {
185
210
  it("should return null for paths within working directory", async () => {
186
- const tool = createWriteFileTool({
187
- fileSystemService,
188
- directoryApproval
189
- });
211
+ const tool = createWriteFileTool(getFileSystemService);
190
212
  const testFile = path.join(tempDir, "new-file.txt");
191
- const override = await tool.getApprovalOverride?.({
192
- file_path: testFile,
193
- content: "test"
194
- });
213
+ const override = await tool.getApprovalOverride?.(
214
+ tool.inputSchema.parse({ file_path: testFile, content: "test" }),
215
+ toolContext
216
+ );
195
217
  expect(override).toBeNull();
196
218
  });
197
219
  it("should return directory access approval for external paths", async () => {
198
- const tool = createWriteFileTool({
199
- fileSystemService,
200
- directoryApproval
201
- });
220
+ const tool = createWriteFileTool(getFileSystemService);
202
221
  const externalPath = "/external/project/new.ts";
203
- const override = await tool.getApprovalOverride?.({
204
- file_path: externalPath,
205
- content: "test"
206
- });
222
+ const override = await tool.getApprovalOverride?.(
223
+ tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
224
+ toolContext
225
+ );
207
226
  expect(override).not.toBeNull();
208
227
  expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
209
228
  const metadata = override?.metadata;
@@ -211,65 +230,69 @@ describe("Directory Approval Integration Tests", () => {
211
230
  expect(metadata?.toolName).toBe("write_file");
212
231
  });
213
232
  it("should return null when external path is session-approved", async () => {
214
- isSessionApprovedMock.mockReturnValue(true);
215
- const tool = createWriteFileTool({
216
- fileSystemService,
217
- directoryApproval
218
- });
233
+ approvalManager.addApprovedDirectory("/external/project", "session");
234
+ const tool = createWriteFileTool(getFileSystemService);
219
235
  const externalPath = "/external/project/new.ts";
220
- const override = await tool.getApprovalOverride?.({
221
- file_path: externalPath,
222
- content: "test"
223
- });
236
+ const override = await tool.getApprovalOverride?.(
237
+ tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
238
+ toolContext
239
+ );
224
240
  expect(override).toBeNull();
225
241
  });
226
242
  });
227
243
  describe("onApprovalGranted", () => {
228
244
  it("should add directory as session-approved when rememberDirectory is true", async () => {
229
- const tool = createWriteFileTool({
230
- fileSystemService,
231
- directoryApproval
232
- });
245
+ const tool = createWriteFileTool(getFileSystemService);
233
246
  const externalPath = "/external/project/new.ts";
234
- await tool.getApprovalOverride?.({ file_path: externalPath, content: "test" });
235
- tool.onApprovalGranted?.({
236
- approvalId: "test-approval",
237
- status: ApprovalStatus.APPROVED,
238
- data: { rememberDirectory: true }
239
- });
240
- expect(addApprovedMock).toHaveBeenCalledWith(
241
- path.dirname(path.resolve(externalPath)),
242
- "session"
247
+ const approvalRequest = await tool.getApprovalOverride?.(
248
+ tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
249
+ toolContext
250
+ );
251
+ expect(approvalRequest).not.toBeNull();
252
+ if (!approvalRequest) {
253
+ throw new Error("Expected approval request");
254
+ }
255
+ tool.onApprovalGranted?.(
256
+ {
257
+ approvalId: "test-approval",
258
+ status: ApprovalStatus.APPROVED,
259
+ data: { rememberDirectory: true }
260
+ },
261
+ toolContext,
262
+ approvalRequest
243
263
  );
264
+ expect(
265
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
266
+ ).toBe("session");
244
267
  });
245
268
  });
246
269
  });
247
270
  describe("Edit File Tool", () => {
248
271
  describe("getApprovalOverride", () => {
249
272
  it("should return null for paths within working directory", async () => {
250
- const tool = createEditFileTool({
251
- fileSystemService,
252
- directoryApproval
253
- });
273
+ const tool = createEditFileTool(getFileSystemService);
254
274
  const testFile = path.join(tempDir, "existing.txt");
255
- const override = await tool.getApprovalOverride?.({
256
- file_path: testFile,
257
- old_string: "old",
258
- new_string: "new"
259
- });
275
+ const override = await tool.getApprovalOverride?.(
276
+ tool.inputSchema.parse({
277
+ file_path: testFile,
278
+ old_string: "old",
279
+ new_string: "new"
280
+ }),
281
+ toolContext
282
+ );
260
283
  expect(override).toBeNull();
261
284
  });
262
285
  it("should return directory access approval for external paths", async () => {
263
- const tool = createEditFileTool({
264
- fileSystemService,
265
- directoryApproval
266
- });
286
+ const tool = createEditFileTool(getFileSystemService);
267
287
  const externalPath = "/external/project/existing.ts";
268
- const override = await tool.getApprovalOverride?.({
269
- file_path: externalPath,
270
- old_string: "old",
271
- new_string: "new"
272
- });
288
+ const override = await tool.getApprovalOverride?.(
289
+ tool.inputSchema.parse({
290
+ file_path: externalPath,
291
+ old_string: "old",
292
+ new_string: "new"
293
+ }),
294
+ toolContext
295
+ );
273
296
  expect(override).not.toBeNull();
274
297
  expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
275
298
  const metadata = override?.metadata;
@@ -277,116 +300,129 @@ describe("Directory Approval Integration Tests", () => {
277
300
  expect(metadata?.toolName).toBe("edit_file");
278
301
  });
279
302
  it("should return null when external path is session-approved", async () => {
280
- isSessionApprovedMock.mockReturnValue(true);
281
- const tool = createEditFileTool({
282
- fileSystemService,
283
- directoryApproval
284
- });
303
+ approvalManager.addApprovedDirectory("/external/project", "session");
304
+ const tool = createEditFileTool(getFileSystemService);
285
305
  const externalPath = "/external/project/existing.ts";
286
- const override = await tool.getApprovalOverride?.({
287
- file_path: externalPath,
288
- old_string: "old",
289
- new_string: "new"
290
- });
306
+ const override = await tool.getApprovalOverride?.(
307
+ tool.inputSchema.parse({
308
+ file_path: externalPath,
309
+ old_string: "old",
310
+ new_string: "new"
311
+ }),
312
+ toolContext
313
+ );
291
314
  expect(override).toBeNull();
292
315
  });
293
316
  });
294
317
  });
295
318
  describe("Session vs Once Approval Scenarios", () => {
296
319
  it("should not prompt for subsequent requests after session approval", async () => {
297
- const tool = createReadFileTool({
298
- fileSystemService,
299
- directoryApproval
300
- });
320
+ const tool = createReadFileTool(getFileSystemService);
301
321
  const externalPath1 = "/external/project/file1.ts";
302
322
  const externalPath2 = "/external/project/file2.ts";
303
- let override = await tool.getApprovalOverride?.({ file_path: externalPath1 });
323
+ let override = await tool.getApprovalOverride?.(
324
+ tool.inputSchema.parse({ file_path: externalPath1 }),
325
+ toolContext
326
+ );
304
327
  expect(override).not.toBeNull();
305
- tool.onApprovalGranted?.({
306
- approvalId: "approval-1",
307
- status: ApprovalStatus.APPROVED,
308
- data: { rememberDirectory: true }
309
- });
310
- expect(addApprovedMock).toHaveBeenCalledWith(
311
- path.dirname(path.resolve(externalPath1)),
312
- "session"
328
+ if (!override) {
329
+ throw new Error("Expected approval request");
330
+ }
331
+ tool.onApprovalGranted?.(
332
+ {
333
+ approvalId: "approval-1",
334
+ status: ApprovalStatus.APPROVED,
335
+ data: { rememberDirectory: true }
336
+ },
337
+ toolContext,
338
+ override
339
+ );
340
+ expect(
341
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath1)))
342
+ ).toBe("session");
343
+ override = await tool.getApprovalOverride?.(
344
+ tool.inputSchema.parse({ file_path: externalPath2 }),
345
+ toolContext
313
346
  );
314
- isSessionApprovedMock.mockReturnValue(true);
315
- override = await tool.getApprovalOverride?.({ file_path: externalPath2 });
316
347
  expect(override).toBeNull();
317
348
  });
318
349
  it("should prompt for subsequent requests after once approval", async () => {
319
- const tool = createReadFileTool({
320
- fileSystemService,
321
- directoryApproval
322
- });
350
+ const tool = createReadFileTool(getFileSystemService);
323
351
  const externalPath1 = "/external/project/file1.ts";
324
352
  const externalPath2 = "/external/project/file2.ts";
325
- let override = await tool.getApprovalOverride?.({ file_path: externalPath1 });
353
+ let override = await tool.getApprovalOverride?.(
354
+ tool.inputSchema.parse({ file_path: externalPath1 }),
355
+ toolContext
356
+ );
326
357
  expect(override).not.toBeNull();
327
- tool.onApprovalGranted?.({
328
- approvalId: "approval-1",
329
- status: ApprovalStatus.APPROVED,
330
- data: { rememberDirectory: false }
331
- });
332
- expect(addApprovedMock).toHaveBeenCalledWith(
333
- path.dirname(path.resolve(externalPath1)),
334
- "once"
358
+ if (!override) {
359
+ throw new Error("Expected approval request");
360
+ }
361
+ tool.onApprovalGranted?.(
362
+ {
363
+ approvalId: "approval-1",
364
+ status: ApprovalStatus.APPROVED,
365
+ data: { rememberDirectory: false }
366
+ },
367
+ toolContext,
368
+ override
369
+ );
370
+ expect(
371
+ approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath1)))
372
+ ).toBe("once");
373
+ override = await tool.getApprovalOverride?.(
374
+ tool.inputSchema.parse({ file_path: externalPath2 }),
375
+ toolContext
335
376
  );
336
- isSessionApprovedMock.mockReturnValue(false);
337
- override = await tool.getApprovalOverride?.({ file_path: externalPath2 });
338
377
  expect(override).not.toBeNull();
339
378
  });
340
379
  });
341
380
  describe("Path Containment Scenarios", () => {
342
381
  it("should cover child paths when parent directory is session-approved", async () => {
343
- const tool = createReadFileTool({
344
- fileSystemService,
345
- directoryApproval
346
- });
347
- isSessionApprovedMock.mockImplementation((filePath) => {
348
- const normalizedPath = path.resolve(filePath);
349
- const approvedDir = "/external/project";
350
- return normalizedPath.startsWith(approvedDir + path.sep) || normalizedPath === approvedDir;
351
- });
352
- let override = await tool.getApprovalOverride?.({
353
- file_path: "/external/project/file.ts"
354
- });
382
+ const tool = createReadFileTool(getFileSystemService);
383
+ approvalManager.addApprovedDirectory("/external/project", "session");
384
+ let override = await tool.getApprovalOverride?.(
385
+ tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
386
+ toolContext
387
+ );
355
388
  expect(override).toBeNull();
356
- override = await tool.getApprovalOverride?.({
357
- file_path: "/external/project/deep/nested/file.ts"
358
- });
389
+ override = await tool.getApprovalOverride?.(
390
+ tool.inputSchema.parse({ file_path: "/external/project/deep/nested/file.ts" }),
391
+ toolContext
392
+ );
359
393
  expect(override).toBeNull();
360
394
  });
361
395
  it("should NOT cover sibling directories", async () => {
362
- const tool = createReadFileTool({
363
- fileSystemService,
364
- directoryApproval
365
- });
366
- isSessionApprovedMock.mockImplementation((filePath) => {
367
- const normalizedPath = path.resolve(filePath);
368
- const approvedDir = "/external/sub";
369
- return normalizedPath.startsWith(approvedDir + path.sep) || normalizedPath === approvedDir;
370
- });
371
- let override = await tool.getApprovalOverride?.({ file_path: "/external/sub/file.ts" });
396
+ const tool = createReadFileTool(getFileSystemService);
397
+ approvalManager.addApprovedDirectory("/external/sub", "session");
398
+ let override = await tool.getApprovalOverride?.(
399
+ tool.inputSchema.parse({ file_path: "/external/sub/file.ts" }),
400
+ toolContext
401
+ );
372
402
  expect(override).toBeNull();
373
- override = await tool.getApprovalOverride?.({ file_path: "/external/other/file.ts" });
403
+ override = await tool.getApprovalOverride?.(
404
+ tool.inputSchema.parse({ file_path: "/external/other/file.ts" }),
405
+ toolContext
406
+ );
374
407
  expect(override).not.toBeNull();
375
408
  });
376
409
  });
377
410
  describe("Different External Directories Scenarios", () => {
378
411
  it("should require separate approval for different external directories", async () => {
379
- const tool = createReadFileTool({
380
- fileSystemService,
381
- directoryApproval
382
- });
412
+ const tool = createReadFileTool(getFileSystemService);
383
413
  const dir1Path = "/external/project1/file.ts";
384
414
  const dir2Path = "/external/project2/file.ts";
385
- const override1 = await tool.getApprovalOverride?.({ file_path: dir1Path });
415
+ const override1 = await tool.getApprovalOverride?.(
416
+ tool.inputSchema.parse({ file_path: dir1Path }),
417
+ toolContext
418
+ );
386
419
  expect(override1).not.toBeNull();
387
420
  const metadata1 = override1?.metadata;
388
421
  expect(metadata1?.parentDir).toBe("/external/project1");
389
- const override2 = await tool.getApprovalOverride?.({ file_path: dir2Path });
422
+ const override2 = await tool.getApprovalOverride?.(
423
+ tool.inputSchema.parse({ file_path: dir2Path }),
424
+ toolContext
425
+ );
390
426
  expect(override2).not.toBeNull();
391
427
  const metadata2 = override2?.metadata;
392
428
  expect(metadata2?.parentDir).toBe("/external/project2");
@@ -394,55 +430,58 @@ describe("Directory Approval Integration Tests", () => {
394
430
  });
395
431
  describe("Mixed Operations Scenarios", () => {
396
432
  it("should share directory approval across different file operations", async () => {
397
- const readTool = createReadFileTool({ fileSystemService, directoryApproval });
398
- const writeTool = createWriteFileTool({ fileSystemService, directoryApproval });
399
- const editTool = createEditFileTool({ fileSystemService, directoryApproval });
433
+ const readTool = createReadFileTool(getFileSystemService);
434
+ const writeTool = createWriteFileTool(getFileSystemService);
435
+ const editTool = createEditFileTool(getFileSystemService);
400
436
  const externalDir = "/external/project";
437
+ const approvalRequest = await readTool.getApprovalOverride?.(
438
+ readTool.inputSchema.parse({ file_path: `${externalDir}/file1.ts` }),
439
+ toolContext
440
+ );
441
+ expect(approvalRequest).not.toBeNull();
442
+ if (!approvalRequest) {
443
+ throw new Error("Expected approval request");
444
+ }
445
+ readTool.onApprovalGranted?.(
446
+ {
447
+ approvalId: "approval-1",
448
+ status: ApprovalStatus.APPROVED,
449
+ data: { rememberDirectory: true }
450
+ },
451
+ toolContext,
452
+ approvalRequest
453
+ );
401
454
  expect(
402
- await readTool.getApprovalOverride?.({ file_path: `${externalDir}/file1.ts` })
403
- ).not.toBeNull();
404
- expect(
405
- await writeTool.getApprovalOverride?.({
406
- file_path: `${externalDir}/file2.ts`,
407
- content: "test"
408
- })
409
- ).not.toBeNull();
410
- expect(
411
- await editTool.getApprovalOverride?.({
412
- file_path: `${externalDir}/file3.ts`,
413
- old_string: "a",
414
- new_string: "b"
415
- })
416
- ).not.toBeNull();
417
- isSessionApprovedMock.mockReturnValue(true);
418
- expect(
419
- await readTool.getApprovalOverride?.({ file_path: `${externalDir}/file1.ts` })
420
- ).toBeNull();
421
- expect(
422
- await writeTool.getApprovalOverride?.({
423
- file_path: `${externalDir}/file2.ts`,
424
- content: "test"
425
- })
455
+ await writeTool.getApprovalOverride?.(
456
+ writeTool.inputSchema.parse({
457
+ file_path: `${externalDir}/file2.ts`,
458
+ content: "test"
459
+ }),
460
+ toolContext
461
+ )
426
462
  ).toBeNull();
427
463
  expect(
428
- await editTool.getApprovalOverride?.({
429
- file_path: `${externalDir}/file3.ts`,
430
- old_string: "a",
431
- new_string: "b"
432
- })
464
+ await editTool.getApprovalOverride?.(
465
+ editTool.inputSchema.parse({
466
+ file_path: `${externalDir}/file3.ts`,
467
+ old_string: "a",
468
+ new_string: "b"
469
+ }),
470
+ toolContext
471
+ )
433
472
  ).toBeNull();
434
473
  });
435
474
  });
436
- describe("Without Directory Approval Callbacks", () => {
437
- it("should work without directory approval callbacks (all paths need normal tool confirmation)", async () => {
438
- const tool = createReadFileTool({
439
- fileSystemService,
440
- directoryApproval: void 0
441
- });
442
- const override = await tool.getApprovalOverride?.({
443
- file_path: "/external/project/file.ts"
444
- });
445
- expect(override).not.toBeNull();
475
+ describe("Without ApprovalManager in context", () => {
476
+ it("should throw for external paths", async () => {
477
+ const tool = createReadFileTool(getFileSystemService);
478
+ const contextWithoutApprovalManager = { logger: mockLogger };
479
+ await expect(
480
+ tool.getApprovalOverride?.(
481
+ tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
482
+ contextWithoutApprovalManager
483
+ )
484
+ ).rejects.toBeInstanceOf(DextoRuntimeError);
446
485
  });
447
486
  });
448
487
  });