diff --git a/src/infra/fs-safe.test.ts b/src/infra/fs-safe.test.ts index 9227874cf8a..e55346ef90d 100644 --- a/src/infra/fs-safe.test.ts +++ b/src/infra/fs-safe.test.ts @@ -25,6 +25,54 @@ afterEach(async () => { await tempDirs.cleanup(); }); +describe("writeFileWithinRoot atomic write regression", () => { + it("writes non-zero byte content reliably (regression test for #44372)", async () => { + // Regression test: v2026.3.11 introduced atomic writes but stat was called before sync, + // causing 0-byte files when kernel write buffer wasn't flushed yet + const rootDir = await tempDirs.make("fs-safe-write-test"); + const relativePath = "test-file.txt"; + const testContent = "#!/usr/bin/env python3\nprint('hello world')\n"; + + // Write the file + await writeFileWithinRoot({ + rootDir, + relativePath, + data: testContent, + }); + + // Verify content is not empty + const fullPath = path.join(rootDir, relativePath); + const stat = await fs.stat(fullPath); + expect(stat.size).toBeGreaterThan(0); + expect(stat.size).toBe(testContent.length); + + // Verify actual content matches + const content = await fs.readFile(fullPath, "utf8"); + expect(content).toBe(testContent); + }); + + it("handles multiple rapid writes without 0-byte regression", async () => { + // Regression test: rapid successive writes should all succeed + const rootDir = await tempDirs.make("fs-safe-rapid-write-test"); + const relativePath = "rapid-write-test.txt"; + + for (let i = 0; i < 5; i++) { + const testContent = `Iteration ${i}: ${"x".repeat(1000)}\n`; + + await writeFileWithinRoot({ + rootDir, + relativePath, + data: testContent, + }); + + const fullPath = path.join(rootDir, relativePath); + const stat = await fs.stat(fullPath); + expect(stat.size).toBeGreaterThan(0); + expect(stat.size).toBe(testContent.length); + } + }); +}); + async function expectWriteOpenRaceIsBlocked(params: { slotPath: string; outsideDir: string; diff --git a/src/infra/fs-safe.ts b/src/infra/fs-safe.ts index 6bf0411a631..17247384e04 100644 --- a/src/infra/fs-safe.ts +++ b/src/infra/fs-safe.ts @@ -323,6 +323,9 @@ async function writeTempFileForAtomicReplace(params: { } else { await tempHandle.writeFile(params.data); } + // Sync to disk before stat to ensure all data is flushed and stat returns correct size + // Without this, stat may return 0 bytes if the kernel hasn't flushed the write buffer yet + await tempHandle.sync(); return await tempHandle.stat(); } finally { await tempHandle.close().catch(() => {});