feat(util): extract dsh-atomic-write and migrate settings-local writes
writeFileAtomic: exclusive-create random-suffix temp + rename carrying the caller-stated mode; settings-local persistSection now consumes it. The credentials-local store shares it next.
This commit is contained in:
48
packages/util/atomic-write/tests/atomic-write.spec.ts
Normal file
48
packages/util/atomic-write/tests/atomic-write.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { lstat, mkdir, mkdtemp, readFile, readdir, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { writeFileAtomic } from '../src/index.ts'
|
||||
|
||||
async function scratch(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), 'dsh-atomic-write-'))
|
||||
}
|
||||
|
||||
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', { mode: 0o600 })
|
||||
expect(await readFile(target, 'utf8')).toBe('a: 1\n')
|
||||
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')
|
||||
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('leaves no temp sibling and rethrows when the rename fails', async () => {
|
||||
const dir = await scratch()
|
||||
const target = join(dir, 'occupied')
|
||||
await mkdir(target)
|
||||
await expect(writeFileAtomic(target, 'content', { mode: 0o600 })).rejects.toThrow()
|
||||
expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([])
|
||||
})
|
||||
})
|
||||
18
packages/util/atomic-write/tests/invariant.spec.ts
Normal file
18
packages/util/atomic-write/tests/invariant.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import * as AtomicWriteInvariant from '../src/invariant.ts'
|
||||
|
||||
describe('atomic-write invariant companion', () => {
|
||||
it('registers its explained empty runtime invariant', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InvariantService)
|
||||
const fiber = await ctx.plugin(AtomicWriteInvariant)
|
||||
|
||||
expect(() => {
|
||||
ctx.invariants.register('@deepseek-ai/dsh-atomic-write', () => {})
|
||||
}).toThrow(/already registered/)
|
||||
await fiber.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user