feat: add canonical typed tool outputs

This commit is contained in:
Tianyi Cui
2026-07-21 03:08:35 +08:00
parent 8500974fd4
commit 66c36e7325
173 changed files with 3298 additions and 954 deletions
+14 -14
View File
@@ -67,7 +67,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await writeFile(join(dir, 'a.txt'), 'original')
const result = await call('write', { file_path: 'a.txt', content: 'clobber' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('original')
})
@@ -85,7 +85,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await writeFile(join(dir, 'a.txt'), 'changed-externally') // out-of-band change
const result = await call('write', { file_path: 'a.txt', content: 'replaced' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(result.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
})
})
@@ -102,7 +102,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await writeFile(join(dir, 'bin'), Buffer.from([0x00, 0x01, 0x02]))
const result = await call('read', { file_path: 'bin' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_TEXT' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_TEXT' } })
})
it('paginates a multi-line file with offset/limit', async () => {
@@ -127,7 +127,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await writeFile(join(dir, 'a.txt'), 'hello world')
const result = await call('edit', { file_path: 'a.txt', old_string: 'world', new_string: 'there' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello world')
})
@@ -151,7 +151,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await writeFile(join(dir, 'a.txt'), 'goodbye') // out-of-band change removes 'world'
const result = await call('edit', { file_path: 'a.txt', old_string: 'world', new_string: 'there' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(result.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
})
it('rejects an ambiguous match without replace_all', async () => {
@@ -159,7 +159,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
await call('read', { file_path: 'a.txt' })
const result = await call('edit', { file_path: 'a.txt', old_string: 'a', new_string: 'b' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_AMBIGUOUS_EDIT' })
expect(result.error).toMatchObject({ info: { code: 'FS_AMBIGUOUS_EDIT' } })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('a a a')
})
@@ -187,7 +187,7 @@ describe('default deployment (with dsh-fs-policy)', () => {
// The model-facing edit still rejects: the read did not emit fs/observed.
const result = await call('edit', { file_path: 'a.txt', old_string: 'world', new_string: 'there' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } })
})
})
@@ -260,14 +260,14 @@ describe('bare provider (no dsh-fs-policy)', () => {
it('edit of a MISSING target reports FS_STALE_VERSION even on the unguarded path', async () => {
const result = await call('edit', { file_path: 'missing.txt', old_string: 'a', new_string: 'b' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(result.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
})
it('edit still enforces literal-match codes (FS_EDIT_NOT_FOUND), unrelated to freshness', async () => {
await writeFile(join(dir, 'a.txt'), 'hello world')
const result = await call('edit', { file_path: 'a.txt', old_string: 'absent', new_string: 'x' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_EDIT_NOT_FOUND' })
expect(result.error).toMatchObject({ info: { code: 'FS_EDIT_NOT_FOUND' } })
})
it('neither write nor edit stats in the tool on the bare path', async () => {
@@ -350,11 +350,11 @@ describe('signal, concurrency, and the fs/observed contract', () => {
await writeFile(join(dir, 'a.txt'), 'hello')
const read = await callSig(AbortSignal.abort(), 'read', { file_path: 'a.txt' })
expect(read.isError).toBe(true)
expect(read.error).toMatchObject({ code: 'FS_ABORTED' })
expect(read.error).toMatchObject({ info: { code: 'FS_ABORTED' } })
const write = await callSig(AbortSignal.abort(), 'write', { file_path: 'new.txt', content: 'x' })
expect(write.isError).toBe(true)
expect(write.error).toMatchObject({ code: 'FS_ABORTED' })
expect(write.error).toMatchObject({ info: { code: 'FS_ABORTED' } })
await expect(readFile(join(dir, 'new.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
// Read first (un-aborted, SAME session owner) so the edit clears the
@@ -363,7 +363,7 @@ describe('signal, concurrency, and the fs/observed contract', () => {
expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false)
const edit = await callSig(AbortSignal.abort(), 'edit', { file_path: 'a.txt', old_string: 'hello', new_string: 'bye' })
expect(edit.isError).toBe(true)
expect(edit.error).toMatchObject({ code: 'FS_ABORTED' })
expect(edit.error).toMatchObject({ info: { code: 'FS_ABORTED' } })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello') // unchanged
})
@@ -378,7 +378,7 @@ describe('signal, concurrency, and the fs/observed contract', () => {
])
const errors = [one, two].filter(r => r.isError)
expect(errors).toHaveLength(1)
expect(errors[0]?.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(errors[0]?.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
// The world is consistent: exactly one edit landed.
const onDisk = await readFile(join(dir, 'a.txt'), 'utf8')
expect(onDisk === 'ONE value here' || onDisk === 'base TWO here').toBe(true)
@@ -407,7 +407,7 @@ describe('signal, concurrency, and the fs/observed contract', () => {
new_string: 'edited',
})
expect(edit.isError).toBe(true)
expect(edit.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(edit.error).toMatchObject({ info: { code: 'FS_STALE_VERSION' } })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('newer current content\n')
})
+29 -9
View File
@@ -161,6 +161,13 @@ describe('read tool', () => {
fs.files.set('key:a.txt', 'hello\nworld')
const result = await call(ctx, 'read', { file_path: 'a.txt' })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected read success')
expect(result.value).toEqual({
path: '/abs/a.txt',
offset: 1,
lines: [{ number: 1, text: 'hello' }, { number: 2, text: 'world' }],
totalLines: 2,
})
expect(text(result)).toBe(`<path>/abs/a.txt</path>
<type>file</type>
<content>
@@ -171,6 +178,15 @@ describe('read tool', () => {
</content>`)
})
it('returns an explicit empty canonical line window for an empty file', async () => {
const { ctx, fs } = await setup()
fs.files.set('key:empty.txt', '')
const result = await call(ctx, 'read', { file_path: 'empty.txt' })
if (result.isError) throw new Error('expected empty read success')
expect(result.value).toEqual({ path: '/abs/empty.txt', offset: 1, lines: [], totalLines: 0 })
expect(text(result)).toContain('(End of file - total 0 lines)')
})
it('rejects a non-positive offset via arg validation', async () => {
const { ctx } = await setup()
const result = await call(ctx, 'read', { file_path: 'a.txt', offset: 0 })
@@ -226,7 +242,7 @@ describe('read tool', () => {
const { ctx } = await setup()
const result = await call(ctx, 'read', { file_path: 'missing.txt' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_FOUND' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
})
it('rejects a non-regular target', async () => {
@@ -235,7 +251,7 @@ describe('read tool', () => {
fs.stat = async () => ({ version: FsVersion('v1'), type: 'directory' })
const result = await call(ctx, 'read', { file_path: 'd' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_REGULAR_FILE' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } })
})
it('streams a large file (size at/above the cap) instead of reading whole', async () => {
@@ -301,6 +317,8 @@ describe('write tool', () => {
const { ctx, fs } = await setup()
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'hi' }, { session: { header: {} } })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected write success')
expect(result.value).toEqual({ path: '/abs/a.txt', operation: 'create', before: null, after: 'hi' })
expect(text(result)).toContain('Created file')
expect(fs.writeIntents).toEqual([{ kind: 'createIfAbsent' }])
})
@@ -317,7 +335,7 @@ describe('write tool', () => {
fs.rejectWith = new FsError('blocked', 'FS_STALE_VERSION')
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'hi' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ name: 'FsError', code: 'FS_STALE_VERSION' })
expect(result.error).toMatchObject({ info: { name: 'FsError', code: 'FS_STALE_VERSION' } })
})
})
@@ -328,6 +346,8 @@ describe('edit tool', () => {
fs.files.set('key:a.txt', 'a')
await call(ctx, 'read', { file_path: 'a.txt' }, { session })
const result = await call(ctx, 'edit', { file_path: 'a.txt', old_string: 'a', new_string: 'b' }, { session })
if (result.isError) throw new Error('expected edit success')
expect(result.value).toEqual({ path: '/abs/a.txt', before: 'a', after: 'b' })
expect(text(result)).toBe('The file /abs/a.txt has been updated successfully.')
})
@@ -366,7 +386,7 @@ describe('edit tool', () => {
fs.files.set('key:a.txt', 'hello')
const result = await call(ctx, 'edit', { file_path: 'a.txt', old_string: 'a', new_string: 'b' }, { session: { header: {} } })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(result.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } })
})
})
@@ -464,27 +484,27 @@ describe('result-time contextual diff (meta + presentResult)', () => {
expect(view).toEqual({ card: 'diff', title: 'Write a.txt', diffs: [{ path: 'a.txt', oldText: 'a\nb\nc\nOLD\nd\ne\nf', newText: 'a\nb\nc\nNEW\nd\ne\nf' }] })
})
it('write CREATE: no before-version → no meta, but presentResult still renders a whole-file diff card', async () => {
// A create has no prior content (no `meta`), yet the completed card must be a `diff` — an
it('write CREATE: an empty applied-diff projection still falls back to the whole-file diff card', async () => {
// A create has no prior content, yet the completed card must be a `diff` — an
// ACP tool_call_update.content REPLACES the call's content, so a non-diff result would
// clobber the pending new-file diff.
const { ctx } = await setup()
const session = { header: {} }
const result = await call(ctx, 'write', { file_path: 'new.txt', content: 'fresh\n' }, { session })
expect(result.isError).toBe(false)
expect(result.meta).toBeUndefined()
expect(result.meta).toEqual({ diffs: [] })
const view = ctx.tools.get('write')?.presentResult?.({ file_path: 'new.txt', content: 'fresh\n' }, result)
expect(view).toEqual({ card: 'diff', title: 'Write new.txt', diffs: [{ path: 'new.txt', oldText: null, newText: 'fresh\n' }] })
})
it('write OVERWRITE with identical content: a before exists but yields no hunk → no meta, presentResult falls back to a whole-file diff', async () => {
it('write OVERWRITE with identical content: an empty applied-diff projection falls back to a whole-file diff', async () => {
const { ctx, fs } = await setup()
const session = { header: {} }
fs.files.set('key:a.txt', 'same\n')
await call(ctx, 'read', { file_path: 'a.txt' }, { session })
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'same\n' }, { session })
expect(result.isError).toBe(false)
expect(result.meta).toBeUndefined()
expect(result.meta).toEqual({ diffs: [] })
const view = ctx.tools.get('write')?.presentResult?.({ file_path: 'a.txt', content: 'same\n' }, result)
expect(view).toEqual({ card: 'diff', title: 'Write a.txt', diffs: [{ path: 'a.txt', oldText: null, newText: 'same\n' }] })
})