From fdf5e08548a8350c3ffb53eafc44c1d43ef65725 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 13 Jul 2026 11:55:51 +0800 Subject: [PATCH] test(tool-fs): cover stale read observation fail-closed --- packages/fs/tool-fs/tests/integration.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/fs/tool-fs/tests/integration.spec.ts b/packages/fs/tool-fs/tests/integration.spec.ts index c0973197eb..3ee23a2590 100644 --- a/packages/fs/tool-fs/tests/integration.spec.ts +++ b/packages/fs/tool-fs/tests/integration.spec.ts @@ -398,6 +398,35 @@ describe('signal, concurrency, and the fs/observed contract', () => { expect(onDisk === 'ONE value here' || onDisk === 'base TWO here').toBe(true) }) + it('a stale observed version from an older read fails closed at edit CAS', async () => { + await writeFile(join(dir, 'a.txt'), 'older content\n') + const target = await ctx.fs.resolve('a.txt') + const firstInfo = await ctx.fs.stat(target) + if (!firstInfo) throw new Error('expected first stat') + + expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false) + + await writeFile(join(dir, 'a.txt'), 'newer current content\n') + const secondInfo = await ctx.fs.stat(target) + if (!secondInfo) throw new Error('expected second stat') + expect(secondInfo.version).not.toBe(firstInfo.version) + expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false) + + // Simulate an older concurrent read finishing last and overwriting the + // observed-state WeakMap with the stale version it saw before the external + // file change. The provider's in-lock CAS is still the safety boundary. + ctx.emit('fs/observed', target, firstInfo.version, { agent: { session } }) + + const edit = await callOwned('edit', { + file_path: 'a.txt', + old_string: 'newer', + new_string: 'edited', + }) + expect(edit.isError).toBe(true) + expect(edit.error).toMatchObject({ code: 'FS_STALE_VERSION' }) + expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('newer current content\n') + }) + it('a throwing fs/observed listener surfaces as isError, but the mutation already hit disk', async () => { // fs/observed is a plain ctx.emit AFTER the write succeeded; a throwing // listener cannot roll the write back — it only turns the tool result into