@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.
Files changed (95) hide show
  1. package/dist/directory-approval.cjs +98 -0
  2. package/dist/directory-approval.d.ts +24 -0
  3. package/dist/directory-approval.d.ts.map +1 -0
  4. package/dist/directory-approval.integration.test.cjs +175 -390
  5. package/dist/directory-approval.integration.test.d.ts +14 -2
  6. package/dist/directory-approval.integration.test.d.ts.map +1 -0
  7. package/dist/directory-approval.integration.test.js +178 -390
  8. package/dist/directory-approval.js +63 -0
  9. package/dist/edit-file-tool.cjs +109 -120
  10. package/dist/edit-file-tool.d.ts +22 -9
  11. package/dist/edit-file-tool.d.ts.map +1 -0
  12. package/dist/edit-file-tool.js +116 -110
  13. package/dist/edit-file-tool.test.cjs +109 -29
  14. package/dist/edit-file-tool.test.d.ts +7 -2
  15. package/dist/edit-file-tool.test.d.ts.map +1 -0
  16. package/dist/edit-file-tool.test.js +109 -29
  17. package/dist/error-codes.cjs +4 -0
  18. package/dist/error-codes.d.ts +6 -3
  19. package/dist/error-codes.d.ts.map +1 -0
  20. package/dist/error-codes.js +4 -0
  21. package/dist/errors.cjs +48 -0
  22. package/dist/errors.d.ts +20 -7
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +48 -0
  25. package/dist/file-tool-types.d.ts +8 -40
  26. package/dist/file-tool-types.d.ts.map +1 -0
  27. package/dist/filesystem-service.cjs +325 -10
  28. package/dist/filesystem-service.d.ts +41 -12
  29. package/dist/filesystem-service.d.ts.map +1 -0
  30. package/dist/filesystem-service.js +326 -11
  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 +32 -46
  36. package/dist/glob-files-tool.d.ts +19 -9
  37. package/dist/glob-files-tool.d.ts.map +1 -0
  38. package/dist/glob-files-tool.js +33 -47
  39. package/dist/grep-content-tool.cjs +40 -45
  40. package/dist/grep-content-tool.d.ts +28 -9
  41. package/dist/grep-content-tool.d.ts.map +1 -0
  42. package/dist/grep-content-tool.js +41 -46
  43. package/dist/index.cjs +6 -3
  44. package/dist/index.d.cts +852 -14
  45. package/dist/index.d.ts +11 -5
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +4 -2
  48. package/dist/path-validator.cjs +28 -2
  49. package/dist/path-validator.d.ts +20 -9
  50. package/dist/path-validator.d.ts.map +1 -0
  51. package/dist/path-validator.js +28 -2
  52. package/dist/path-validator.test.d.ts +7 -2
  53. package/dist/path-validator.test.d.ts.map +1 -0
  54. package/dist/read-file-tool.cjs +26 -59
  55. package/dist/read-file-tool.d.ts +19 -9
  56. package/dist/read-file-tool.d.ts.map +1 -0
  57. package/dist/read-file-tool.js +27 -50
  58. package/dist/tool-factory-config.cjs +61 -0
  59. package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
  60. package/dist/tool-factory-config.d.ts.map +1 -0
  61. package/dist/tool-factory-config.js +36 -0
  62. package/dist/tool-factory.cjs +123 -0
  63. package/dist/tool-factory.d.ts +4 -0
  64. package/dist/tool-factory.d.ts.map +1 -0
  65. package/dist/tool-factory.js +102 -0
  66. package/dist/types.d.ts +82 -18
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/write-file-tool.cjs +93 -99
  69. package/dist/write-file-tool.d.ts +22 -9
  70. package/dist/write-file-tool.d.ts.map +1 -0
  71. package/dist/write-file-tool.js +97 -91
  72. package/dist/write-file-tool.test.cjs +139 -33
  73. package/dist/write-file-tool.test.d.ts +7 -2
  74. package/dist/write-file-tool.test.d.ts.map +1 -0
  75. package/dist/write-file-tool.test.js +139 -33
  76. package/package.json +5 -4
  77. package/dist/directory-approval.integration.test.d.cts +0 -2
  78. package/dist/edit-file-tool.d.cts +0 -17
  79. package/dist/edit-file-tool.test.d.cts +0 -2
  80. package/dist/error-codes.d.cts +0 -32
  81. package/dist/errors.d.cts +0 -112
  82. package/dist/file-tool-types.d.cts +0 -46
  83. package/dist/filesystem-service.d.cts +0 -112
  84. package/dist/filesystem-service.test.d.cts +0 -2
  85. package/dist/glob-files-tool.d.cts +0 -17
  86. package/dist/grep-content-tool.d.cts +0 -17
  87. package/dist/path-validator.d.cts +0 -97
  88. package/dist/path-validator.test.d.cts +0 -2
  89. package/dist/read-file-tool.d.cts +0 -17
  90. package/dist/tool-provider.cjs +0 -123
  91. package/dist/tool-provider.d.cts +0 -77
  92. package/dist/tool-provider.js +0 -99
  93. package/dist/types.d.cts +0 -178
  94. package/dist/write-file-tool.d.cts +0 -17
  95. package/dist/write-file-tool.test.d.cts +0 -2
@@ -2,25 +2,52 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import * as path from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
+ import {
6
+ ApprovalManager,
7
+ DextoRuntimeError
8
+ } from "@dexto/core";
9
+ import { FileSystemService } from "./filesystem-service.js";
5
10
  import { createReadFileTool } from "./read-file-tool.js";
6
11
  import { createWriteFileTool } from "./write-file-tool.js";
7
12
  import { createEditFileTool } from "./edit-file-tool.js";
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
- });
13
+ const createMockLogger = () => {
14
+ const noopAsync = async () => void 0;
15
+ const logger = {
16
+ debug: vi.fn(),
17
+ silly: vi.fn(),
18
+ info: vi.fn(),
19
+ warn: vi.fn(),
20
+ error: vi.fn(),
21
+ trackException: vi.fn(),
22
+ createChild: () => logger,
23
+ createFileOnlyChild: () => logger,
24
+ setLevel: vi.fn(),
25
+ getLevel: () => "info",
26
+ getLogFilePath: () => null,
27
+ destroy: noopAsync
28
+ };
29
+ return logger;
30
+ };
31
+ function createToolContext(logger, approval) {
32
+ return {
33
+ logger,
34
+ services: {
35
+ approval,
36
+ search: {},
37
+ resources: {},
38
+ prompts: {},
39
+ mcp: {},
40
+ taskForker: null
41
+ }
42
+ };
43
+ }
17
44
  describe("Directory Approval Integration Tests", () => {
18
45
  let mockLogger;
19
46
  let tempDir;
20
47
  let fileSystemService;
21
- let directoryApproval;
22
- let isSessionApprovedMock;
23
- let addApprovedMock;
48
+ let approvalManager;
49
+ let toolContext;
50
+ const getFileSystemService = async (_context) => fileSystemService;
24
51
  beforeEach(async () => {
25
52
  mockLogger = createMockLogger();
26
53
  const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
@@ -38,12 +65,14 @@ describe("Directory Approval Integration Tests", () => {
38
65
  mockLogger
39
66
  );
40
67
  await fileSystemService.initialize();
41
- isSessionApprovedMock = vi.fn().mockReturnValue(false);
42
- addApprovedMock = vi.fn();
43
- directoryApproval = {
44
- isSessionApproved: isSessionApprovedMock,
45
- addApproved: addApprovedMock
46
- };
68
+ approvalManager = new ApprovalManager(
69
+ {
70
+ permissions: { mode: "manual" },
71
+ elicitation: { enabled: true }
72
+ },
73
+ mockLogger
74
+ );
75
+ toolContext = createToolContext(mockLogger, approvalManager);
47
76
  vi.clearAllMocks();
48
77
  });
49
78
  afterEach(async () => {
@@ -52,397 +81,156 @@ describe("Directory Approval Integration Tests", () => {
52
81
  } catch {
53
82
  }
54
83
  });
55
- describe("Read File Tool", () => {
56
- describe("getApprovalOverride", () => {
57
- it("should return null for paths within working directory (no prompt needed)", async () => {
58
- const tool = createReadFileTool({
59
- fileSystemService,
60
- directoryApproval
61
- });
62
- const testFile = path.join(tempDir, "test.txt");
63
- await fs.writeFile(testFile, "test content");
64
- const override = await tool.getApprovalOverride?.({ file_path: testFile });
65
- expect(override).toBeNull();
66
- });
67
- it("should return directory access approval for external paths", async () => {
68
- const tool = createReadFileTool({
69
- fileSystemService,
70
- directoryApproval
71
- });
72
- const externalPath = "/external/project/file.ts";
73
- const override = await tool.getApprovalOverride?.({ file_path: externalPath });
74
- expect(override).not.toBeNull();
75
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
76
- const metadata = override?.metadata;
77
- expect(metadata?.path).toBe(path.resolve(externalPath));
78
- expect(metadata?.parentDir).toBe(path.dirname(path.resolve(externalPath)));
79
- expect(metadata?.operation).toBe("read");
80
- expect(metadata?.toolName).toBe("read_file");
81
- });
82
- it("should return null when external path is session-approved", async () => {
83
- isSessionApprovedMock.mockReturnValue(true);
84
- const tool = createReadFileTool({
85
- fileSystemService,
86
- directoryApproval
87
- });
88
- 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?.({});
99
- expect(override).toBeNull();
100
- });
101
- });
102
- describe("onApprovalGranted", () => {
103
- it("should add directory as session-approved when rememberDirectory is true", async () => {
104
- const tool = createReadFileTool({
105
- fileSystemService,
106
- directoryApproval
107
- });
108
- 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"
118
- );
119
- });
120
- it("should add directory as once-approved when rememberDirectory is false", async () => {
121
- const tool = createReadFileTool({
122
- fileSystemService,
123
- directoryApproval
124
- });
125
- 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"
135
- );
136
- });
137
- it("should default to once-approved when rememberDirectory is not specified", async () => {
138
- const tool = createReadFileTool({
139
- fileSystemService,
140
- directoryApproval
141
- });
142
- 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"
152
- );
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();
167
- });
84
+ describe("getApprovalOverride", () => {
85
+ it("should return null for paths within config-allowed roots", async () => {
86
+ const tool = createReadFileTool(getFileSystemService);
87
+ const overrideFn = tool.approval?.override;
88
+ expect(overrideFn).toBeDefined();
89
+ const testFile = path.join(tempDir, "test.txt");
90
+ await fs.writeFile(testFile, "test content");
91
+ const metadata = await overrideFn(
92
+ tool.inputSchema.parse({ file_path: testFile }),
93
+ toolContext
94
+ );
95
+ expect(metadata).toBeNull();
168
96
  });
169
- describe("execute", () => {
170
- it("should read file contents within working directory", async () => {
171
- const tool = createReadFileTool({
172
- fileSystemService,
173
- directoryApproval
174
- });
175
- const testFile = path.join(tempDir, "readable.txt");
176
- await fs.writeFile(testFile, "Hello, world!\nLine 2");
177
- const result = await tool.execute({ file_path: testFile }, {});
178
- expect(result.content).toBe("Hello, world!\nLine 2");
179
- expect(result.lines).toBe(2);
97
+ it("should return directory access metadata for external paths", async () => {
98
+ const tool = createReadFileTool(getFileSystemService);
99
+ const overrideFn = tool.approval?.override;
100
+ expect(overrideFn).toBeDefined();
101
+ const externalPath = "/external/project/file.ts";
102
+ const metadata = await overrideFn(
103
+ tool.inputSchema.parse({ file_path: externalPath }),
104
+ toolContext
105
+ );
106
+ expect(metadata).not.toBeNull();
107
+ expect(metadata).toMatchObject({
108
+ type: "directory_access",
109
+ metadata: {
110
+ path: path.resolve(externalPath),
111
+ parentDir: path.dirname(path.resolve(externalPath)),
112
+ operation: "read",
113
+ toolName: "read_file"
114
+ }
180
115
  });
181
116
  });
182
- });
183
- describe("Write File Tool", () => {
184
- describe("getApprovalOverride", () => {
185
- it("should return null for paths within working directory", async () => {
186
- const tool = createWriteFileTool({
187
- fileSystemService,
188
- directoryApproval
189
- });
190
- const testFile = path.join(tempDir, "new-file.txt");
191
- const override = await tool.getApprovalOverride?.({
192
- file_path: testFile,
193
- content: "test"
194
- });
195
- expect(override).toBeNull();
196
- });
197
- it("should return directory access approval for external paths", async () => {
198
- const tool = createWriteFileTool({
199
- fileSystemService,
200
- directoryApproval
201
- });
202
- const externalPath = "/external/project/new.ts";
203
- const override = await tool.getApprovalOverride?.({
204
- file_path: externalPath,
205
- content: "test"
206
- });
207
- expect(override).not.toBeNull();
208
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
209
- const metadata = override?.metadata;
210
- expect(metadata?.operation).toBe("write");
211
- expect(metadata?.toolName).toBe("write_file");
212
- });
213
- it("should return null when external path is session-approved", async () => {
214
- isSessionApprovedMock.mockReturnValue(true);
215
- const tool = createWriteFileTool({
216
- fileSystemService,
217
- directoryApproval
218
- });
219
- const externalPath = "/external/project/new.ts";
220
- const override = await tool.getApprovalOverride?.({
221
- file_path: externalPath,
222
- content: "test"
223
- });
224
- expect(override).toBeNull();
225
- });
117
+ it("should return null when external path is session-approved", async () => {
118
+ approvalManager.addApprovedDirectory("/external/project", "session");
119
+ const tool = createReadFileTool(getFileSystemService);
120
+ const overrideFn = tool.approval?.override;
121
+ expect(overrideFn).toBeDefined();
122
+ const externalPath = "/external/project/file.ts";
123
+ const metadata = await overrideFn(
124
+ tool.inputSchema.parse({ file_path: externalPath }),
125
+ toolContext
126
+ );
127
+ expect(metadata).toBeNull();
226
128
  });
227
- describe("onApprovalGranted", () => {
228
- it("should add directory as session-approved when rememberDirectory is true", async () => {
229
- const tool = createWriteFileTool({
230
- fileSystemService,
231
- directoryApproval
232
- });
233
- 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"
243
- );
244
- });
129
+ it("should still return metadata when external path is once-approved (prompt again)", async () => {
130
+ approvalManager.addApprovedDirectory("/external/project", "once");
131
+ const tool = createReadFileTool(getFileSystemService);
132
+ const overrideFn = tool.approval?.override;
133
+ expect(overrideFn).toBeDefined();
134
+ const externalPath = "/external/project/file.ts";
135
+ const metadata = await overrideFn(
136
+ tool.inputSchema.parse({ file_path: externalPath }),
137
+ toolContext
138
+ );
139
+ expect(metadata).not.toBeNull();
245
140
  });
246
141
  });
247
- describe("Edit File Tool", () => {
248
- describe("getApprovalOverride", () => {
249
- it("should return null for paths within working directory", async () => {
250
- const tool = createEditFileTool({
251
- fileSystemService,
252
- directoryApproval
253
- });
254
- 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
- });
260
- expect(override).toBeNull();
261
- });
262
- it("should return directory access approval for external paths", async () => {
263
- const tool = createEditFileTool({
264
- fileSystemService,
265
- directoryApproval
266
- });
267
- 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
- });
273
- expect(override).not.toBeNull();
274
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
275
- const metadata = override?.metadata;
276
- expect(metadata?.operation).toBe("edit");
277
- expect(metadata?.toolName).toBe("edit_file");
142
+ describe("Different tool operations", () => {
143
+ it("should label write operations correctly", async () => {
144
+ const tool = createWriteFileTool(getFileSystemService);
145
+ const overrideFn = tool.approval?.override;
146
+ expect(overrideFn).toBeDefined();
147
+ const externalPath = "/external/project/new.ts";
148
+ const metadata = await overrideFn(
149
+ tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
150
+ toolContext
151
+ );
152
+ expect(metadata).not.toBeNull();
153
+ expect(metadata).toMatchObject({
154
+ type: "directory_access",
155
+ metadata: {
156
+ path: path.resolve(externalPath),
157
+ parentDir: path.dirname(path.resolve(externalPath)),
158
+ operation: "write",
159
+ toolName: "write_file"
160
+ }
278
161
  });
279
- it("should return null when external path is session-approved", async () => {
280
- isSessionApprovedMock.mockReturnValue(true);
281
- const tool = createEditFileTool({
282
- fileSystemService,
283
- directoryApproval
284
- });
285
- const externalPath = "/external/project/existing.ts";
286
- const override = await tool.getApprovalOverride?.({
162
+ });
163
+ it("should label edit operations correctly", async () => {
164
+ const tool = createEditFileTool(getFileSystemService);
165
+ const overrideFn = tool.approval?.override;
166
+ expect(overrideFn).toBeDefined();
167
+ const externalPath = "/external/project/existing.ts";
168
+ const metadata = await overrideFn(
169
+ tool.inputSchema.parse({
287
170
  file_path: externalPath,
288
171
  old_string: "old",
289
172
  new_string: "new"
290
- });
291
- expect(override).toBeNull();
292
- });
293
- });
294
- });
295
- describe("Session vs Once Approval Scenarios", () => {
296
- it("should not prompt for subsequent requests after session approval", async () => {
297
- const tool = createReadFileTool({
298
- fileSystemService,
299
- directoryApproval
300
- });
301
- const externalPath1 = "/external/project/file1.ts";
302
- const externalPath2 = "/external/project/file2.ts";
303
- let override = await tool.getApprovalOverride?.({ file_path: externalPath1 });
304
- 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"
173
+ }),
174
+ toolContext
313
175
  );
314
- isSessionApprovedMock.mockReturnValue(true);
315
- override = await tool.getApprovalOverride?.({ file_path: externalPath2 });
316
- expect(override).toBeNull();
317
- });
318
- it("should prompt for subsequent requests after once approval", async () => {
319
- const tool = createReadFileTool({
320
- fileSystemService,
321
- directoryApproval
322
- });
323
- const externalPath1 = "/external/project/file1.ts";
324
- const externalPath2 = "/external/project/file2.ts";
325
- let override = await tool.getApprovalOverride?.({ file_path: externalPath1 });
326
- expect(override).not.toBeNull();
327
- tool.onApprovalGranted?.({
328
- approvalId: "approval-1",
329
- status: ApprovalStatus.APPROVED,
330
- data: { rememberDirectory: false }
176
+ expect(metadata).not.toBeNull();
177
+ expect(metadata).toMatchObject({
178
+ type: "directory_access",
179
+ metadata: {
180
+ path: path.resolve(externalPath),
181
+ parentDir: path.dirname(path.resolve(externalPath)),
182
+ operation: "edit",
183
+ toolName: "edit_file"
184
+ }
331
185
  });
332
- expect(addApprovedMock).toHaveBeenCalledWith(
333
- path.dirname(path.resolve(externalPath1)),
334
- "once"
335
- );
336
- isSessionApprovedMock.mockReturnValue(false);
337
- override = await tool.getApprovalOverride?.({ file_path: externalPath2 });
338
- expect(override).not.toBeNull();
339
186
  });
340
187
  });
341
- describe("Path Containment Scenarios", () => {
188
+ describe("Path containment scenarios", () => {
342
189
  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
- });
355
- expect(override).toBeNull();
356
- override = await tool.getApprovalOverride?.({
357
- file_path: "/external/project/deep/nested/file.ts"
358
- });
359
- expect(override).toBeNull();
190
+ const tool = createReadFileTool(getFileSystemService);
191
+ const overrideFn = tool.approval?.override;
192
+ expect(overrideFn).toBeDefined();
193
+ approvalManager.addApprovedDirectory("/external/project", "session");
194
+ const metadata1 = await overrideFn(
195
+ tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
196
+ toolContext
197
+ );
198
+ expect(metadata1).toBeNull();
199
+ const metadata2 = await overrideFn(
200
+ tool.inputSchema.parse({ file_path: "/external/project/deep/nested/file.ts" }),
201
+ toolContext
202
+ );
203
+ expect(metadata2).toBeNull();
360
204
  });
361
205
  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" });
372
- expect(override).toBeNull();
373
- override = await tool.getApprovalOverride?.({ file_path: "/external/other/file.ts" });
374
- expect(override).not.toBeNull();
375
- });
376
- });
377
- describe("Different External Directories Scenarios", () => {
378
- it("should require separate approval for different external directories", async () => {
379
- const tool = createReadFileTool({
380
- fileSystemService,
381
- directoryApproval
382
- });
383
- const dir1Path = "/external/project1/file.ts";
384
- const dir2Path = "/external/project2/file.ts";
385
- const override1 = await tool.getApprovalOverride?.({ file_path: dir1Path });
386
- expect(override1).not.toBeNull();
387
- const metadata1 = override1?.metadata;
388
- expect(metadata1?.parentDir).toBe("/external/project1");
389
- const override2 = await tool.getApprovalOverride?.({ file_path: dir2Path });
390
- expect(override2).not.toBeNull();
391
- const metadata2 = override2?.metadata;
392
- expect(metadata2?.parentDir).toBe("/external/project2");
393
- });
394
- });
395
- describe("Mixed Operations Scenarios", () => {
396
- 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 });
400
- const externalDir = "/external/project";
401
- 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
- })
426
- ).toBeNull();
427
- expect(
428
- await editTool.getApprovalOverride?.({
429
- file_path: `${externalDir}/file3.ts`,
430
- old_string: "a",
431
- new_string: "b"
432
- })
433
- ).toBeNull();
206
+ const tool = createReadFileTool(getFileSystemService);
207
+ const overrideFn = tool.approval?.override;
208
+ expect(overrideFn).toBeDefined();
209
+ approvalManager.addApprovedDirectory("/external/sub", "session");
210
+ const metadata1 = await overrideFn(
211
+ tool.inputSchema.parse({ file_path: "/external/sub/file.ts" }),
212
+ toolContext
213
+ );
214
+ expect(metadata1).toBeNull();
215
+ const metadata2 = await overrideFn(
216
+ tool.inputSchema.parse({ file_path: "/external/other/file.ts" }),
217
+ toolContext
218
+ );
219
+ expect(metadata2).not.toBeNull();
434
220
  });
435
221
  });
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();
222
+ describe("Without ApprovalManager in context", () => {
223
+ it("should throw for external paths", async () => {
224
+ const tool = createReadFileTool(getFileSystemService);
225
+ const overrideFn = tool.approval?.override;
226
+ expect(overrideFn).toBeDefined();
227
+ const contextWithoutApprovalManager = { logger: mockLogger };
228
+ await expect(
229
+ overrideFn(
230
+ tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
231
+ contextWithoutApprovalManager
232
+ )
233
+ ).rejects.toBeInstanceOf(DextoRuntimeError);
446
234
  });
447
235
  });
448
236
  });