diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index e0ae61224f..6f7fde6307 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -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(); }); }); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index ce4af9f38a..c2978f7065 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -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>; + 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.` + ); + } }