fix(fs): observe absence before guarded recreation
This commit is contained in:
@@ -234,37 +234,33 @@ describe('default deployment (with dsh-fs-policy)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleted observed target (fail-closed corner)', () => {
|
||||
it('a deleted observed file stays un-writable and un-editable in-session: the remedy cannot unblock it', async () => {
|
||||
describe('deleted observed target', () => {
|
||||
it('a failed reread records absence so write can safely recreate the file', async () => {
|
||||
await writeFile(join(dir, 'a.txt'), 'original')
|
||||
await call('read', { file_path: 'a.txt' })
|
||||
await rm(join(dir, 'a.txt')) // out-of-band deletion
|
||||
|
||||
// Edit of the missing target: stale (the missing-target path shares the
|
||||
// stale code and the re-read remedy).
|
||||
// The original positive observation still protects the first mutation.
|
||||
const edit = await call('edit', { file_path: 'a.txt', old_string: 'original', new_string: 'x' })
|
||||
expect(edit.isError).toBe(true)
|
||||
expect(edit.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
|
||||
|
||||
// Re-reading the missing file FAILS with FS_NOT_FOUND and records no
|
||||
// observation, so the retried edit fails identically: the observed entry
|
||||
// is never cleared for a deleted target.
|
||||
const reread = await call('read', { file_path: 'a.txt' })
|
||||
expect(reread.isError).toBe(true)
|
||||
expect(reread.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
|
||||
const retriedEdit = await call('edit', { file_path: 'a.txt', old_string: 'original', new_string: 'x' })
|
||||
expect(retriedEdit.isError).toBe(true)
|
||||
expect(retriedEdit.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
|
||||
|
||||
// Write cannot recreate it either: the stale observation still forces
|
||||
// replaceIfVersion, which rejects a missing target ("file no longer exists").
|
||||
const write = await call('write', { file_path: 'a.txt', content: 'fresh' })
|
||||
const write = await call('write', { file_path: 'a.txt', content: 'premature' })
|
||||
expect(write.isError).toBe(true)
|
||||
expect(write.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
|
||||
|
||||
// The dead end lifts once the file exists again and is freshly observed.
|
||||
await writeFile(join(dir, 'a.txt'), 'restored')
|
||||
expect((await call('read', { file_path: 'a.txt' })).isError).toBe(false)
|
||||
// A read-not-found is an authoritative negative observation for this
|
||||
// owner. It still fails as a read, but changes the next write guard.
|
||||
const reread = await call('read', { file_path: 'a.txt' })
|
||||
expect(reread.isError).toBe(true)
|
||||
expect(reread.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
|
||||
|
||||
// Absence never authorizes edit: there is no content/version to edit.
|
||||
const retriedEdit = await call('edit', { file_path: 'a.txt', old_string: 'original', new_string: 'x' })
|
||||
expect(retriedEdit.isError).toBe(true)
|
||||
expect(retriedEdit.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
|
||||
|
||||
// The retried write uses createIfAbsent; the provider remains responsible
|
||||
// for rejecting a concurrent creator at publication time.
|
||||
const recovered = await call('write', { file_path: 'a.txt', content: 'fresh' })
|
||||
expect(recovered.isError).toBe(false)
|
||||
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('fresh')
|
||||
@@ -294,6 +290,20 @@ describe('default deployment (with dsh-fs-policy)', () => {
|
||||
expect(statSpy).not.toHaveBeenCalled()
|
||||
statSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('a missing read still stats once and its recovery write stats zero times', async () => {
|
||||
const statSpy = vi.spyOn(ctx.fs, 'stat')
|
||||
const missing = await call('read', { file_path: 'missing.txt' })
|
||||
expect(missing.isError).toBe(true)
|
||||
expect(missing.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
|
||||
expect(statSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
statSpy.mockClear()
|
||||
const created = await call('write', { file_path: 'missing.txt', content: 'fresh' })
|
||||
expect(created.isError).toBe(false)
|
||||
expect(statSpy).not.toHaveBeenCalled()
|
||||
statSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -482,7 +492,7 @@ describe('signal, concurrency, and the fs/observed contract', () => {
|
||||
expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false)
|
||||
|
||||
// Reproduce an older concurrent read winning the observation race.
|
||||
ctx.emit('fs/observed', target, firstInfo.version, { agent: { session } })
|
||||
ctx.emit('fs/observed', target, { kind: 'present', version: firstInfo.version }, { agent: { session } })
|
||||
|
||||
const edit = await callOwned('edit', {
|
||||
file_path: 'a.txt',
|
||||
|
||||
Reference in New Issue
Block a user