Merge 79bb75f90db044e887492b648b0da141c7893b32 into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Andy Tien 2026-03-21 10:05:08 +08:00 committed by GitHub
commit ce1487f4fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 51 additions and 0 deletions

View File

@ -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;

View File

@ -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(() => {});