import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { editTool } from '../src/modules/netaclaw/tools/builtin/edit.js'; describe('editTool', () => { const tempDirs: string[] = []; afterEach(() => { tempDirs.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true })); }); const createFile = (content: string) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-edit-tool-')); tempDirs.push(dir); const filePath = path.join(dir, 'target.txt'); fs.writeFileSync(filePath, content, 'utf8'); return filePath; }; it('applies multiple non-overlapping edits against the original file', async () => { const filePath = createFile('alpha\nbeta\ngamma\n'); const result = await editTool.execute('tool-1', { path: filePath, edits: [ { oldText: 'alpha', newText: 'ALPHA' }, { oldText: 'gamma', newText: 'GAMMA' }, ], }); expect(fs.readFileSync(filePath, 'utf8')).toBe('ALPHA\nbeta\nGAMMA\n'); expect(result).toEqual(expect.objectContaining({ type: 'text', text: expect.stringContaining('已编辑 2 处'), })); expect((result as any).text).toContain('Diff:'); }); it('rejects duplicate oldText matches instead of editing ambiguously', async () => { const filePath = createFile('same\nsame\n'); const result = await editTool.execute('tool-1', { path: filePath, edits: [{ oldText: 'same', newText: 'changed' }], }); expect(fs.readFileSync(filePath, 'utf8')).toBe('same\nsame\n'); expect((result as any).text).toContain('Found 2 occurrences'); }); it('keeps legacy oldText/newText input compatible', async () => { const filePath = createFile('before\n'); await editTool.execute('tool-1', { path: filePath, oldText: 'before', newText: 'after', } as any); expect(fs.readFileSync(filePath, 'utf8')).toBe('after\n'); }); it('resolves relative paths from runtime session cwd', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-edit-tool-')); tempDirs.push(root); fs.writeFileSync(path.join(root, 'target.txt'), 'before\n', 'utf8'); await editTool.execute('tool-1', { path: 'target.txt', edits: [{ oldText: 'before', newText: 'after' }], _netaRuntime: { sessionCwd: root }, } as any); expect(fs.readFileSync(path.join(root, 'target.txt'), 'utf8')).toBe('after\n'); }); it('uses Pi-style fuzzy matching for trailing whitespace and smart punctuation', async () => { const filePath = createFile('const title = “Hello” \n'); const result = await editTool.execute('tool-1', { path: filePath, edits: [{ oldText: 'const title = "Hello"', newText: 'const title = "Hi"' }], }); expect(fs.readFileSync(filePath, 'utf8')).toBe('const title = "Hi"\n'); expect((result as any).text).toContain('首个变更行: 1'); }); });