fix(filesystem): make move_file fail instead of overwriting the destination (#4630)

The move_file tool description and README both state the operation fails if
the destination already exists, but the handler called fs.rename directly,
which silently overwrites the destination. Since this server ships no delete
tool, move_file effectively provided unadvertised, irreversible file
deletion: moving any file onto an existing target destroyed the target.

Add a moveFile helper in lib.ts that rejects when the destination already
exists (checked with lstat, so an existing symlink is detected rather than
followed) and only renames when the target is free, and use it from the
move_file handler. Behaviour now matches the documented contract.

Fixes #4628
This commit is contained in:
eeshsaxena 2026-08-28 22:28:51 +05:30 • committed by GitHub
commit 18ce197639
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,6 +14,7 @@ import {
getFileStats,
readFileContent,
writeFileContent,
moveFile,
// Search & filtering functions
searchFilesWithValidation,
// File editing functions
@ -339,6 +340,29 @@ describe('Lib Functions', () => {
});
});
describe('moveFile', () => {
it('moves the file when the destination does not exist', async () => {
const enoent = Object.assign(new Error('not found'), { code: 'ENOENT' });
mockFs.lstat.mockRejectedValueOnce(enoent);
mockFs.rename.mockResolvedValueOnce(undefined);
await moveFile('/test/source.txt', '/test/dest.txt');
expect(mockFs.rename).toHaveBeenCalledWith('/test/source.txt', '/test/dest.txt');
});
it('fails without overwriting when the destination already exists', async () => {
// lstat resolving means the destination is occupied.
mockFs.lstat.mockResolvedValueOnce({} as any);
await expect(moveFile('/test/source.txt', '/test/dest.txt')).rejects.toThrow(
'Destination already exists'
);
expect(mockFs.rename).not.toHaveBeenCalled();
});
});
});
describe('Search & Filtering Functions', () => {