Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -49,12 +49,37 @@ describe('resolveSpec', () => {
|
||||
describe('boot and reads', () => {
|
||||
it('resolves defaults over an absent file and reports writable', async () => {
|
||||
const dir = await tempDir()
|
||||
const ctx = await boot({ path: join(dir, 'settings.yaml'), watch: false })
|
||||
const path = join(dir, 'settings.yaml')
|
||||
const ctx = await boot({ path, watch: false })
|
||||
const scope = ctx.settings.register(settingsNamespace('ui-theme'), ThemeSchema, {
|
||||
base: { fontSize: 16 },
|
||||
})
|
||||
expect(scope.get()).toEqual({ theme: 'dark', fontSize: 16 })
|
||||
expect(ctx.settings.writable).toBe(true)
|
||||
expect(ctx.settings.documentPath).toBe(path)
|
||||
})
|
||||
|
||||
it('prepares an absent owner-only document without changing resolved settings', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, 'nested', 'settings.yaml')
|
||||
const ctx = await boot({ path, watch: false })
|
||||
const scope = ctx.settings.register(settingsNamespace('ui-theme'), ThemeSchema)
|
||||
|
||||
await expect(ctx.settings.prepareDocument()).resolves.toBe(path)
|
||||
expect(await readFile(path, 'utf8')).toBe('')
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o600)
|
||||
expect(scope.get()).toEqual({ theme: 'dark', fontSize: 14 })
|
||||
})
|
||||
|
||||
it('preparing an existing document preserves its contents', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, 'settings.yaml')
|
||||
const contents = 'ui-theme:\n theme: light\n'
|
||||
await writeFile(path, contents)
|
||||
const ctx = await boot({ path, watch: false })
|
||||
|
||||
await expect(ctx.settings.prepareDocument()).resolves.toBe(path)
|
||||
expect(await readFile(path, 'utf8')).toBe(contents)
|
||||
})
|
||||
|
||||
it('reads sections from an existing yaml document', async () => {
|
||||
@@ -78,6 +103,7 @@ describe('boot and reads', () => {
|
||||
it('defaults the file location under the configured harness home', async () => {
|
||||
const dir = await tempDir()
|
||||
const ctx = await boot({ dshHome: dir, watch: false })
|
||||
expect(ctx.settings.documentPath).toBe(join(dir, 'settings.yaml'))
|
||||
const scope = ctx.settings.register(settingsNamespace('ui-theme'), ThemeSchema)
|
||||
await scope.update({ theme: 'light' })
|
||||
const written = await readFile(join(dir, 'settings.yaml'), 'utf8')
|
||||
|
||||
@@ -11,6 +11,10 @@ import { SettingsLocal } from '../src/index.ts'
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
failTempWrite: false,
|
||||
failDocumentCreate: false,
|
||||
holdDocumentCreate: false,
|
||||
documentCreateStarted: undefined as (() => void) | undefined,
|
||||
continueDocumentCreate: undefined as Promise<void> | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
@@ -18,6 +22,15 @@ vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
writeFile: (async (path: unknown, ...rest: never[]) => {
|
||||
if (state.holdDocumentCreate && String(path).endsWith('settings.yaml')) {
|
||||
state.holdDocumentCreate = false
|
||||
state.documentCreateStarted!()
|
||||
await state.continueDocumentCreate!
|
||||
}
|
||||
if (state.failDocumentCreate && String(path).endsWith('settings.yaml')) {
|
||||
state.failDocumentCreate = false
|
||||
throw Object.assign(new Error('ENOSPC: injected document create failure'), { code: 'ENOSPC' })
|
||||
}
|
||||
if (state.failTempWrite && String(path).endsWith('.tmp')) {
|
||||
state.failTempWrite = false
|
||||
throw Object.assign(new Error('ENOSPC: injected writeFile failure'), { code: 'ENOSPC' })
|
||||
@@ -33,6 +46,10 @@ const cleanups: Array<() => Promise<void>> = []
|
||||
|
||||
afterEach(async () => {
|
||||
state.failTempWrite = false
|
||||
state.failDocumentCreate = false
|
||||
state.holdDocumentCreate = false
|
||||
state.documentCreateStarted = undefined
|
||||
state.continueDocumentCreate = undefined
|
||||
while (cleanups.length > 0) await cleanups.pop()!()
|
||||
})
|
||||
|
||||
@@ -51,6 +68,49 @@ async function boot(config: ConstructorParameters<typeof SettingsLocal>[1]): Pro
|
||||
}
|
||||
|
||||
describe('writer-lock failure cleanup', () => {
|
||||
it('skips publication when an in-flight document create completes during teardown', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, 'settings.yaml')
|
||||
const ctx = new Context()
|
||||
const fiber = ctx.plugin(SettingsLocal, { path, watch: false })
|
||||
cleanups.push(async () => { await fiber.dispose() })
|
||||
await fiber
|
||||
const settings = ctx.settings
|
||||
settings.register(settingsNamespace('alpha'), AlphaSchema)
|
||||
const published: number[] = []
|
||||
ctx.on('settings/document-updated', (_ns, revision) => { published.push(revision) })
|
||||
let markStarted!: () => void
|
||||
const started = new Promise<void>((resolve) => { markStarted = resolve })
|
||||
let releaseCreate!: () => void
|
||||
state.continueDocumentCreate = new Promise<void>((resolve) => { releaseCreate = resolve })
|
||||
state.documentCreateStarted = markStarted
|
||||
state.holdDocumentCreate = true
|
||||
|
||||
const preparing = settings.prepareDocument()
|
||||
await started
|
||||
let disposed = false
|
||||
const disposing = fiber.dispose()
|
||||
void disposing.then(() => { disposed = true })
|
||||
await vi.waitFor(() => {
|
||||
expect((settings as unknown as { closed: boolean }).closed).toBe(true)
|
||||
})
|
||||
expect(disposed).toBe(false)
|
||||
releaseCreate()
|
||||
await expect(preparing).resolves.toBe(path)
|
||||
await disposing
|
||||
expect(await readFile(path, 'utf8')).toBe('')
|
||||
expect(published).toEqual([])
|
||||
})
|
||||
|
||||
it('surfaces an exclusive document-create failure and releases the lock', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, 'settings.yaml')
|
||||
const ctx = await boot({ path, watch: false })
|
||||
state.failDocumentCreate = true
|
||||
await expect(ctx.settings.prepareDocument()).rejects.toThrow(/ENOSPC/)
|
||||
await expect(access(`${path}.lock`)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('cleans up the temp file and releases the lock when the write fails mid-cycle', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, 'settings.yaml')
|
||||
|
||||
Reference in New Issue
Block a user