7735b2abd5
Replaces fsyncSync(tempFileDescriptor) with tolerantFsyncSync, allowing EPERM/EACCES/ENOTSUP/EINVAL during fsync while still propagating real errors. Adds an optional deps.fsyncSync injection point used solely by the new EPERM tolerance regression tests. Without this fix, plugin startup itself can fail on synced folders because writeFileAtomically is used by config migrations and posthog activity state — the same EPERM-on-fsync failure pattern reported for team_create.
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from "fs"
|
|
import { join } from "path"
|
|
import { tmpdir } from "os"
|
|
import { writeFileAtomically } from "./write-file-atomically"
|
|
|
|
const testDir = join(tmpdir(), "write-file-atomically-test-" + Date.now())
|
|
|
|
beforeEach(() => {
|
|
mkdirSync(testDir, { recursive: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
})
|
|
|
|
describe("writeFileAtomically", () => {
|
|
it("writes content to a new file", () => {
|
|
// given
|
|
const filePath = join(testDir, "new-file.txt")
|
|
const content = "hello world"
|
|
|
|
// when
|
|
writeFileAtomically(filePath, content)
|
|
|
|
// then
|
|
expect(existsSync(filePath)).toBe(true)
|
|
expect(readFileSync(filePath, "utf-8")).toBe(content)
|
|
})
|
|
|
|
it("#given target file exists #when writeFileAtomically called #then overwrites successfully", () => {
|
|
// given
|
|
const filePath = join(testDir, "existing-file.txt")
|
|
const originalContent = "original content"
|
|
const newContent = "new content"
|
|
writeFileSync(filePath, originalContent, "utf-8")
|
|
|
|
// when
|
|
writeFileAtomically(filePath, newContent)
|
|
|
|
// then
|
|
expect(existsSync(filePath)).toBe(true)
|
|
expect(readFileSync(filePath, "utf-8")).toBe(newContent)
|
|
expect(existsSync(`${filePath}.tmp`)).toBe(false)
|
|
})
|
|
|
|
it("#given parent directory does not exist #when writeFileAtomically called #then throws", () => {
|
|
// given
|
|
const filePath = join(testDir, "nonexistent", "deep", "file.txt")
|
|
|
|
// when/then
|
|
expect(() => writeFileAtomically(filePath, "content")).toThrow()
|
|
})
|
|
|
|
it("#given fsync fails with EPERM (synced folder) #when writeFileAtomically called #then write succeeds", () => {
|
|
// given
|
|
const filePath = join(testDir, "synced-folder.txt")
|
|
const content = "content from a synced folder where fsync is rejected"
|
|
|
|
// when
|
|
writeFileAtomically(filePath, content, {
|
|
fsyncSync: () => {
|
|
const error = new Error("EPERM: operation not permitted, fsync") as NodeJS.ErrnoException
|
|
error.code = "EPERM"
|
|
throw error
|
|
},
|
|
})
|
|
|
|
// then
|
|
expect(existsSync(filePath)).toBe(true)
|
|
expect(readFileSync(filePath, "utf-8")).toBe(content)
|
|
})
|
|
|
|
it("#given fsync fails with EIO (real I/O error) #when writeFileAtomically called #then propagates the error", () => {
|
|
// given
|
|
const filePath = join(testDir, "io-error.txt")
|
|
|
|
// when/then
|
|
expect(() =>
|
|
writeFileAtomically(filePath, "content", {
|
|
fsyncSync: () => {
|
|
const error = new Error("EIO: input/output error") as NodeJS.ErrnoException
|
|
error.code = "EIO"
|
|
throw error
|
|
},
|
|
}),
|
|
).toThrow("EIO")
|
|
})
|
|
})
|