mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-05 04:00:17 +00:00
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
import { lstat, mkdtemp, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { withFileLock, writeFileAtomic } from '../src/index.ts'
|
|
|
|
const state = vi.hoisted(() => ({
|
|
failLockCreateWithEPERM: false,
|
|
renameAttempts: 0,
|
|
renameFailures: [] as string[],
|
|
}))
|
|
|
|
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('node:fs/promises')>()
|
|
return {
|
|
...actual,
|
|
rename: (async (...args: Parameters<typeof actual.rename>) => {
|
|
state.renameAttempts += 1
|
|
const code = state.renameFailures.shift()
|
|
if (code !== undefined) {
|
|
if (code === 'NO_CODE') throw new Error('injected rename failure without a code')
|
|
throw Object.assign(new Error(`${code}: injected rename failure`), { code })
|
|
}
|
|
return actual.rename(...args)
|
|
}),
|
|
writeFile: (async (path: unknown, ...rest: never[]) => {
|
|
if (state.failLockCreateWithEPERM && String(path).endsWith('.lock')) {
|
|
state.failLockCreateWithEPERM = false
|
|
throw Object.assign(new Error('EPERM: injected exclusive-create failure'), { code: 'EPERM' })
|
|
}
|
|
return (actual.writeFile as (path: unknown, ...args: never[]) => Promise<void>)(path, ...rest)
|
|
}) as typeof actual.writeFile,
|
|
}
|
|
})
|
|
|
|
const scratchDirs: string[] = []
|
|
|
|
afterEach(async () => {
|
|
vi.useRealTimers()
|
|
vi.restoreAllMocks()
|
|
state.failLockCreateWithEPERM = false
|
|
state.renameAttempts = 0
|
|
state.renameFailures.length = 0
|
|
await Promise.all(scratchDirs.splice(0).map(dir => rm(dir, {
|
|
force: true,
|
|
maxRetries: 10,
|
|
recursive: true,
|
|
retryDelay: 20,
|
|
})))
|
|
})
|
|
|
|
async function scratch(): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-atomic-write-'))
|
|
scratchDirs.push(dir)
|
|
return dir
|
|
}
|
|
|
|
/** Resolve once the lockfile exists, so contention is measured against a held lock. */
|
|
async function waitForLock(lockPath: string): Promise<void> {
|
|
for (;;) {
|
|
try {
|
|
await stat(lockPath)
|
|
return
|
|
} catch {
|
|
await new Promise(resolve => setTimeout(resolve, 5))
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('writeFileAtomic', () => {
|
|
it('creates the file and its parents with exactly the stated mode', async () => {
|
|
const dir = await scratch()
|
|
const target = join(dir, 'nested', 'deep', 'doc.yaml')
|
|
await writeFileAtomic(target, 'a: 1\n', { dirMode: 0o700, mode: 0o600 })
|
|
expect(await readFile(target, 'utf8')).toBe('a: 1\n')
|
|
if (process.platform !== 'win32') {
|
|
expect((await stat(dirname(target))).mode & 0o777).toBe(0o700)
|
|
expect((await stat(target)).mode & 0o777).toBe(0o600)
|
|
}
|
|
})
|
|
|
|
it('replaces existing content and narrows a wider-permission file to the stated mode', async () => {
|
|
const dir = await scratch()
|
|
const target = join(dir, 'doc.yaml')
|
|
await writeFile(target, 'old', { mode: 0o644 })
|
|
await writeFileAtomic(target, 'new', { mode: 0o600 })
|
|
expect(await readFile(target, 'utf8')).toBe('new')
|
|
if (process.platform !== 'win32') expect((await stat(target)).mode & 0o777).toBe(0o600)
|
|
})
|
|
|
|
it('replaces a symlinked target itself without writing through to the referent', async () => {
|
|
const dir = await scratch()
|
|
const victim = join(dir, 'victim')
|
|
await writeFile(victim, 'victim-content')
|
|
const target = join(dir, 'doc.yaml')
|
|
await symlink(victim, target)
|
|
await writeFileAtomic(target, 'replaced', { mode: 0o600 })
|
|
expect((await lstat(target)).isSymbolicLink()).toBe(false)
|
|
expect(await readFile(target, 'utf8')).toBe('replaced')
|
|
expect(await readFile(victim, 'utf8')).toBe('victim-content')
|
|
})
|
|
|
|
it('retries transient Windows rename interference and commits the replacement', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
vi.useFakeTimers()
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
await writeFile(target, 'old')
|
|
state.renameFailures.push('EACCES', 'EBUSY', 'EPERM')
|
|
|
|
const replacement = writeFileAtomic(target, 'new', { mode: 0o600 })
|
|
await vi.waitFor(() => { expect(state.renameAttempts).toBeGreaterThan(0) })
|
|
await vi.runAllTimersAsync()
|
|
await replacement
|
|
|
|
expect(state.renameAttempts).toBe(4)
|
|
expect(await readFile(target, 'utf8')).toBe('new')
|
|
expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([])
|
|
})
|
|
|
|
it('leaves no temp sibling after bounded Windows rename retries expire', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
vi.useFakeTimers()
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
await writeFile(target, 'old')
|
|
state.renameFailures.push(...Array.from({ length: 9 }, () => 'EPERM'))
|
|
|
|
const replacement = writeFileAtomic(target, 'new', { mode: 0o600 })
|
|
await vi.waitFor(() => { expect(state.renameAttempts).toBeGreaterThan(0) })
|
|
await vi.runAllTimersAsync()
|
|
await expect(replacement).rejects.toMatchObject({ code: 'EPERM' })
|
|
|
|
expect(state.renameAttempts).toBe(9)
|
|
expect(await readFile(target, 'utf8')).toBe('old')
|
|
expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([])
|
|
})
|
|
|
|
it('does not retry a Windows rename failure without a transient code', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
state.renameFailures.push('NO_CODE')
|
|
|
|
await expect(writeFileAtomic(target, 'new', { mode: 0o600 })).rejects.toThrow(/without a code/)
|
|
expect(state.renameAttempts).toBe(1)
|
|
expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([])
|
|
})
|
|
|
|
it('does not retry rename permission failures outside Windows', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
state.renameFailures.push('EPERM')
|
|
|
|
await expect(writeFileAtomic(target, 'new', { mode: 0o600 })).rejects.toMatchObject({ code: 'EPERM' })
|
|
expect(state.renameAttempts).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('withFileLock', () => {
|
|
it('retries EPERM only when the lock path currently exists', async () => {
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
const lockPath = `${target}.lock`
|
|
await writeFile(lockPath, 'holder\n')
|
|
const release = setTimeout(() => { void rm(lockPath, { force: true }) }, 50)
|
|
state.failLockCreateWithEPERM = true
|
|
let called = false
|
|
|
|
try {
|
|
await withFileLock(target, async () => { called = true })
|
|
} finally {
|
|
clearTimeout(release)
|
|
}
|
|
expect(called).toBe(true)
|
|
})
|
|
|
|
it('preserves EPERM when no lock path exists', async () => {
|
|
const dir = await scratch()
|
|
const operation = vi.fn(async () => {})
|
|
state.failLockCreateWithEPERM = true
|
|
|
|
await expect(withFileLock(join(dir, 'document'), operation)).rejects.toMatchObject({ code: 'EPERM' })
|
|
expect(operation).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects an invalid parent hierarchy before running the operation', async () => {
|
|
const dir = await scratch()
|
|
const parent = join(dir, 'not-a-directory')
|
|
await writeFile(parent, 'occupied')
|
|
let called = false
|
|
|
|
await expect(withFileLock(join(parent, 'document'), async () => {
|
|
called = true
|
|
})).rejects.toThrow(/ENOENT|ENOTDIR|not a directory/i)
|
|
expect(called).toBe(false)
|
|
})
|
|
|
|
it('waits for the caller-stated limit rather than the protocol default', async () => {
|
|
// An operation whose work includes a network round trip legitimately holds
|
|
// the lock far longer than the render-and-rename the default was sized
|
|
// for. The limit is per call so one such operation cannot fail every other
|
|
// writer of the same file, and a caller that states a short one still
|
|
// fails fast.
|
|
const dir = await scratch()
|
|
const target = join(dir, 'document')
|
|
let release = (): void => {}
|
|
const held = new Promise<void>((resolve) => { release = resolve })
|
|
const holder = withFileLock(target, () => held)
|
|
// The holder owns the lock once its lockfile exists; contending before
|
|
// that would measure nothing.
|
|
await waitForLock(`${target}.lock`)
|
|
|
|
// Elapsed time is the assertion that distinguishes a honoured limit from
|
|
// the ignored argument: without it the contender simply waits out the
|
|
// protocol default and fails with the same message.
|
|
const startedAt = Date.now()
|
|
await expect(withFileLock(target, async () => 'impatient', { waitMs: 50 }))
|
|
.rejects.toThrow(/timed out waiting for the writer lock/)
|
|
expect(Date.now() - startedAt).toBeLessThan(1_000)
|
|
|
|
const patient = withFileLock(target, async () => 'patient', { waitMs: 10_000 })
|
|
release()
|
|
await holder
|
|
expect(await patient).toBe('patient')
|
|
})
|
|
})
|