Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/filesystem/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,43 @@ describe('Lib Functions', () => {
describe('writeFileContent', () => {
it('writes file content', async () => {
mockFs.writeFile.mockResolvedValueOnce(undefined);

mockFs.stat.mockResolvedValueOnce({ size: Buffer.byteLength('new content', 'utf-8') } as any);

await writeFileContent('/test/file.txt', 'new content');

expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'new content', { encoding: "utf-8", flag: 'wx' });
expect(mockFs.stat).toHaveBeenCalledWith('/test/file.txt');
});

// Regression for #4138: write_file silently returns success on Windows
// even though no bytes reach disk. The post-write stat verification turns
// that silent failure into an explicit error.
it('throws when post-write stat finds the file missing (silent write failure)', async () => {
mockFs.writeFile.mockResolvedValueOnce(undefined);
const enoent = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
mockFs.stat.mockRejectedValueOnce(enoent);

await expect(writeFileContent('/test/file.txt', 'payload')).rejects.toThrow(
/reported success but the file is missing on disk/
);
});

it('throws when post-write stat reports a size mismatch', async () => {
mockFs.writeFile.mockResolvedValueOnce(undefined);
// Content is 7 bytes ('payload'); pretend disk shows 0 bytes.
mockFs.stat.mockResolvedValueOnce({ size: 0 } as any);

await expect(writeFileContent('/test/file.txt', 'payload')).rejects.toThrow(
/on-disk size is 0 bytes; expected 7 bytes/
);
});

it('verifies size correctly for multibyte UTF-8 content', async () => {
const content = 'héllo 🌍'; // 12 UTF-8 bytes
mockFs.writeFile.mockResolvedValueOnce(undefined);
mockFs.stat.mockResolvedValueOnce({ size: Buffer.byteLength(content, 'utf-8') } as any);

await expect(writeFileContent('/test/file.txt', content)).resolves.toBeUndefined();
});
});

Expand Down
22 changes: 22 additions & 0 deletions src/filesystem/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,28 @@ export async function writeFileContent(filePath: string, content: string): Promi
throw error;
}
}

// Post-write verification: stat the target and confirm size matches the
// UTF-8 byte length of the written content. Catches silent write failures
// where writeFile resolves successfully but the bytes never reach disk
// (observed on Windows in #4138; the underlying cause varies by host but
// the failure mode — silent success with no file on disk — is the same).
const expectedSize = Buffer.byteLength(content, 'utf-8');
let stats: Awaited<ReturnType<typeof fs.stat>>;
try {
stats = await fs.stat(filePath);
} catch (statError) {
throw new Error(
`Write to ${filePath} reported success but the file is missing on disk. ` +
`This indicates a silent write failure in the host filesystem layer.`
);
}
if (stats.size !== expectedSize) {
throw new Error(
`Write to ${filePath} reported success but on-disk size is ${stats.size} bytes; ` +
`expected ${expectedSize} bytes.`
);
}
}


Expand Down
Loading