Skip to content

Commit 2df6496

Browse files
committed
feat: support relative paths, add tests
1 parent 60e075d commit 2df6496

11 files changed

+462
-304
lines changed

__tests__/applyFileEdits.test.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import {applyFileEdits} from '../utils/fileUtils';
2+
import fs from 'fs/promises';
3+
import path from 'path';
4+
import os from 'os';
5+
6+
describe('applyFileEdits', () => {
7+
const testDir = path.join(process.cwd(), 'sample-file-edits');
8+
const testFilePath = path.join(testDir, 'test.txt');
9+
10+
beforeAll(async () => {
11+
// Create test directory and ensure it's clean
12+
await fs.rm(testDir, {recursive: true, force: true});
13+
await fs.mkdir(testDir, {recursive: true});
14+
});
15+
16+
afterAll(async () => {
17+
// Clean up test directory
18+
await fs.rm(testDir, {recursive: true, force: true});
19+
});
20+
21+
beforeEach(async () => {
22+
// Clean up test file before each test
23+
try {
24+
await fs.unlink(testFilePath);
25+
} catch (error) {
26+
// Ignore errors if file doesn't exist
27+
}
28+
});
29+
30+
describe('file creation', () => {
31+
test('should create new file with content', async () => {
32+
const content = 'Hello, World!';
33+
const result = await applyFileEdits(testDir, testFilePath, undefined, content);
34+
35+
expect(result.newFileCreated).toBe(true);
36+
expect(result.fileExists).toBe(false);
37+
expect(result.validEdits).toBe(true);
38+
expect(result.response).toContain('Successfully created file');
39+
40+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
41+
expect(fileContent).toBe(content);
42+
});
43+
44+
test('should create new file with relative path with ./ prefix', async () => {
45+
const relativePath = './relative-test.txt';
46+
const content = 'Hello from relative path with ./ prefix!';
47+
const result = await applyFileEdits(testDir, relativePath, undefined, content);
48+
49+
expect(result.newFileCreated).toBe(true);
50+
expect(result.fileExists).toBe(false);
51+
expect(result.validEdits).toBe(true);
52+
expect(result.response).toContain('Successfully created file');
53+
54+
const fileContent = await fs.readFile(path.join(testDir, 'relative-test.txt'), 'utf-8');
55+
expect(fileContent).toBe(content);
56+
});
57+
58+
test('should create new file with relative path without ./ prefix', async () => {
59+
const relativePath = 'relative-test-no-prefix.txt';
60+
const content = 'Hello from relative path without ./ prefix!';
61+
const result = await applyFileEdits(testDir, relativePath, undefined, content);
62+
63+
expect(result.newFileCreated).toBe(true);
64+
expect(result.fileExists).toBe(false);
65+
expect(result.validEdits).toBe(true);
66+
expect(result.response).toContain('Successfully created file');
67+
68+
const fileContent = await fs.readFile(
69+
path.join(testDir, 'relative-test-no-prefix.txt'),
70+
'utf-8',
71+
);
72+
expect(fileContent).toBe(content);
73+
});
74+
});
75+
76+
describe('file updates', () => {
77+
test('should update existing file with content', async () => {
78+
// Create initial file
79+
const initialContent = 'Initial content';
80+
await fs.writeFile(testFilePath, initialContent);
81+
82+
const newContent = 'Updated content';
83+
const result = await applyFileEdits(testDir, testFilePath, undefined, newContent);
84+
85+
expect(result.newFileCreated).toBe(false);
86+
expect(result.fileExists).toBe(true);
87+
expect(result.validEdits).toBe(true);
88+
expect(result.response).toContain('Successfully updated file');
89+
90+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
91+
expect(fileContent).toBe(newContent);
92+
});
93+
94+
test('should update existing file with relative path with ./ prefix', async () => {
95+
const relativePath = './relative-test.txt';
96+
const initialContent = 'Initial content with ./ prefix';
97+
await fs.writeFile(path.join(testDir, 'relative-test.txt'), initialContent);
98+
99+
const newContent = 'Updated content with ./ prefix';
100+
const result = await applyFileEdits(testDir, relativePath, undefined, newContent);
101+
102+
expect(result.newFileCreated).toBe(false);
103+
expect(result.fileExists).toBe(true);
104+
expect(result.validEdits).toBe(true);
105+
expect(result.response).toContain('Successfully updated file');
106+
107+
const fileContent = await fs.readFile(path.join(testDir, 'relative-test.txt'), 'utf-8');
108+
expect(fileContent).toBe(newContent);
109+
});
110+
111+
test('should update existing file with relative path without ./ prefix', async () => {
112+
const relativePath = 'relative-test-no-prefix.txt';
113+
const initialContent = 'Initial content without ./ prefix';
114+
await fs.writeFile(path.join(testDir, 'relative-test-no-prefix.txt'), initialContent);
115+
116+
const newContent = 'Updated content without ./ prefix';
117+
const result = await applyFileEdits(testDir, relativePath, undefined, newContent);
118+
119+
expect(result.newFileCreated).toBe(false);
120+
expect(result.fileExists).toBe(true);
121+
expect(result.validEdits).toBe(true);
122+
expect(result.response).toContain('Successfully updated file');
123+
124+
const fileContent = await fs.readFile(
125+
path.join(testDir, 'relative-test-no-prefix.txt'),
126+
'utf-8',
127+
);
128+
expect(fileContent).toBe(newContent);
129+
});
130+
});
131+
132+
describe('file edits', () => {
133+
test('should apply single edit to existing file', async () => {
134+
// Create initial file
135+
const initialContent = 'Hello, World!\nThis is a test.';
136+
await fs.writeFile(testFilePath, initialContent);
137+
138+
const edits = [
139+
{
140+
oldText: 'Hello, World!',
141+
newText: 'Hello, Universe!',
142+
},
143+
];
144+
145+
const result = await applyFileEdits(testDir, testFilePath, edits, undefined);
146+
147+
expect(result.newFileCreated).toBe(false);
148+
expect(result.fileExists).toBe(true);
149+
expect(result.validEdits).toBe(true);
150+
expect(result.response).toContain('Successfully updated file');
151+
152+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
153+
expect(fileContent).toBe('Hello, Universe!\nThis is a test.');
154+
});
155+
156+
test('should apply multiple edits to existing file', async () => {
157+
// Create initial file
158+
const initialContent = 'Hello, World!\nThis is a test.\nGoodbye, World!';
159+
await fs.writeFile(testFilePath, initialContent);
160+
161+
const edits = [
162+
{
163+
oldText: 'Hello, World!',
164+
newText: 'Hello, Universe!',
165+
},
166+
{
167+
oldText: 'Goodbye, World!',
168+
newText: 'Goodbye, Universe!',
169+
},
170+
];
171+
172+
const result = await applyFileEdits(testDir, testFilePath, edits, undefined);
173+
174+
expect(result.newFileCreated).toBe(false);
175+
expect(result.fileExists).toBe(true);
176+
expect(result.validEdits).toBe(true);
177+
expect(result.response).toContain('Successfully updated file');
178+
179+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
180+
expect(fileContent).toBe('Hello, Universe!\nThis is a test.\nGoodbye, Universe!');
181+
});
182+
183+
test('should handle whitespace in edits', async () => {
184+
// Create initial file
185+
const initialContent = ' Hello, World! \n This is a test. ';
186+
await fs.writeFile(testFilePath, initialContent);
187+
188+
const edits = [
189+
{
190+
oldText: 'Hello, World!',
191+
newText: 'Hello, Universe!',
192+
},
193+
];
194+
195+
const result = await applyFileEdits(testDir, testFilePath, edits, undefined);
196+
197+
expect(result.newFileCreated).toBe(false);
198+
expect(result.fileExists).toBe(true);
199+
expect(result.validEdits).toBe(true);
200+
201+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
202+
expect(fileContent).toBe(' Hello, Universe! \n This is a test. ');
203+
});
204+
205+
test('should return error when edit cannot be found', async () => {
206+
// Create initial file
207+
const initialContent = 'Hello, World!';
208+
await fs.writeFile(testFilePath, initialContent);
209+
210+
const edits = [
211+
{
212+
oldText: 'Non-existent text',
213+
newText: 'New text',
214+
},
215+
];
216+
217+
const result = await applyFileEdits(testDir, testFilePath, edits, undefined);
218+
219+
expect(result.validEdits).toBe(false);
220+
expect(result.response).toContain('Could not find exact match for edit');
221+
222+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
223+
expect(fileContent).toBe(initialContent);
224+
});
225+
226+
test('should handle empty file', async () => {
227+
// Create empty file
228+
await fs.writeFile(testFilePath, '');
229+
230+
const edits = [
231+
{
232+
oldText: '',
233+
newText: 'New content',
234+
},
235+
];
236+
237+
const result = await applyFileEdits(testDir, testFilePath, edits, undefined);
238+
239+
expect(result.newFileCreated).toBe(false);
240+
expect(result.fileExists).toBe(true);
241+
expect(result.validEdits).toBe(true);
242+
243+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
244+
expect(fileContent).toBe('New content');
245+
});
246+
});
247+
248+
// Windows-specific tests
249+
if (process.platform === 'win32') {
250+
const windowsTempDir = os.tmpdir();
251+
252+
describe('Windows paths', () => {
253+
test('should handle Windows paths with spaces', async () => {
254+
const spacePath = path.join(windowsTempDir, 'test folder', 'file with spaces.txt');
255+
await fs.mkdir(path.join(windowsTempDir, 'test folder'), {recursive: true});
256+
257+
const content = 'Initial content';
258+
const result = await applyFileEdits(windowsTempDir, spacePath, undefined, content);
259+
260+
expect(result.newFileCreated).toBe(true);
261+
expect(result.fileExists).toBe(false);
262+
expect(result.validEdits).toBe(true);
263+
264+
const fileContent = await fs.readFile(spacePath, 'utf-8');
265+
expect(fileContent).toBe(content);
266+
});
267+
268+
test('should handle Windows paths with special characters', async () => {
269+
const specialPath = path.join(windowsTempDir, 'test#folder', 'file@test.txt');
270+
await fs.mkdir(path.join(windowsTempDir, 'test#folder'), {recursive: true});
271+
272+
const content = 'Content with special chars';
273+
const result = await applyFileEdits(windowsTempDir, specialPath, undefined, content);
274+
275+
expect(result.newFileCreated).toBe(true);
276+
expect(result.fileExists).toBe(false);
277+
expect(result.validEdits).toBe(true);
278+
279+
const fileContent = await fs.readFile(specialPath, 'utf-8');
280+
expect(fileContent).toBe(content);
281+
});
282+
});
283+
}
284+
});

__tests__/diff.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('Diff Output Tests', () => {
2929
await fs.writeFile(testFilePath, initialContent);
3030

3131
fileEditTool = new FileEditTool(
32+
testDir,
3233
[testDir],
3334
model,
3435
AI_PROVIDERS.ANTHROPIC,
@@ -78,6 +79,7 @@ describe('Diff Output Tests', () => {
7879
const newFilePath = path.join(testDir, 'new-file.js');
7980

8081
fileEditTool = new FileEditTool(
82+
testDir,
8183
[testDir],
8284
model,
8385
AI_PROVIDERS.ANTHROPIC,
@@ -126,6 +128,7 @@ describe('Diff Output Tests', () => {
126128
await fs.writeFile(testFilePath, initialContent);
127129

128130
fileEditTool = new FileEditTool(
131+
testDir,
129132
[testDir],
130133
model,
131134
AI_PROVIDERS.ANTHROPIC,

__tests__/file-edit-claude-multiple.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('File Edit Tool with Claude - Multiple Files', () => {
3535
);
3636

3737
fileEditTool = new FileEditTool(
38+
testDir,
3839
[path.join(testDir, '1')],
3940
model,
4041
AI_PROVIDERS.ANTHROPIC,

__tests__/file-edit-claude.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ describe('File Edit Tool with Claude', () => {
3131
);
3232

3333
fileEditTool = new FileEditTool(
34+
testDir,
3435
[path.join(testDir, '1')],
3536
model,
3637
AI_PROVIDERS.ANTHROPIC,
3738
process.env.ANTHROPIC_API_KEY || '',
3839
[path.join(testDir, '1', 'edit-test.js')],
40+
3,
3941
);
4042

4143
// Test editing file in allowed directory
@@ -71,11 +73,13 @@ describe('File Edit Tool with Claude', () => {
7173
);
7274

7375
fileEditTool = new FileEditTool(
76+
testDir,
7477
[path.join(testDir, '1')],
7578
model,
7679
AI_PROVIDERS.ANTHROPIC,
7780
process.env.ANTHROPIC_API_KEY || '',
7881
[path.join(testDir, '1', 'edit-test.js')],
82+
3,
7983
);
8084

8185
// Test editing file in non-allowed directory
@@ -98,11 +102,13 @@ describe('File Edit Tool with Claude', () => {
98102
const newFilePath = path.join(testDir, '1', 'new-file.js');
99103

100104
fileEditTool = new FileEditTool(
105+
testDir,
101106
[path.join(testDir, '1')],
102107
model,
103108
AI_PROVIDERS.ANTHROPIC,
104109
process.env.ANTHROPIC_API_KEY || '',
105110
[path.join(testDir, '1', 'edit-test.js')],
111+
3,
106112
);
107113

108114
// Test creating a new file

__tests__/file-edit-openai-multiple.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('File Edit Tool with OpenAI - Multiple Files', () => {
3434
);
3535

3636
fileEditTool = new FileEditTool(
37+
testDir,
3738
[path.join(testDir, '1')],
3839
model,
3940
AI_PROVIDERS.OPENAI,

0 commit comments

Comments
 (0)