Direct models now use bundled ripgrep for workspace discovery, read files through resumable pages, and edit multiple files with apply_patch. Ask can search and read while Execute retains write access, and fixed total input limits no longer constrain these tools. Release note: 直连模型现可使用随包 ripgrep 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { LocalWorkspaceAccess } from '../workspace'
|
|
import { applyWorkspacePatch } from './workspace-apply-patch'
|
|
|
|
const temporaryDirectories: string[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories.splice(0).map((path) =>
|
|
rm(path, { recursive: true, force: true })
|
|
)
|
|
)
|
|
})
|
|
|
|
async function createWorkspace(): Promise<string> {
|
|
const path = await mkdtemp(join(tmpdir(), 'goodbuddy-apply-patch-'))
|
|
temporaryDirectories.push(path)
|
|
return path
|
|
}
|
|
|
|
describe('workspace apply patch', () => {
|
|
it('validates and applies add, update, and delete operations', async () => {
|
|
const root = await createWorkspace()
|
|
await mkdir(join(root, 'src'))
|
|
await writeFile(join(root, 'src', 'app.ts'), 'const old = 1\n', 'utf8')
|
|
await writeFile(join(root, 'obsolete.txt'), 'remove me\n', 'utf8')
|
|
|
|
const result = await applyWorkspacePatch(
|
|
[
|
|
'*** Begin Patch',
|
|
'*** Update File: src/app.ts',
|
|
'@@',
|
|
'-const old = 1',
|
|
'+const current = 2',
|
|
'*** Add File: nested/new.ts',
|
|
'+export const added = true',
|
|
'*** Delete File: obsolete.txt',
|
|
'*** End Patch'
|
|
].join('\n'),
|
|
new LocalWorkspaceAccess(root),
|
|
new AbortController().signal
|
|
)
|
|
|
|
expect(result).toContain('3 file(s)')
|
|
await expect(readFile(join(root, 'src', 'app.ts'), 'utf8')).resolves.toBe(
|
|
'const current = 2\n'
|
|
)
|
|
await expect(readFile(join(root, 'nested', 'new.ts'), 'utf8')).resolves.toBe(
|
|
'export const added = true'
|
|
)
|
|
await expect(readFile(join(root, 'obsolete.txt'), 'utf8')).rejects.toMatchObject({
|
|
code: 'ENOENT'
|
|
})
|
|
})
|
|
|
|
it('does not modify any file when validation fails', async () => {
|
|
const root = await createWorkspace()
|
|
await writeFile(join(root, 'first.txt'), 'original\n', 'utf8')
|
|
|
|
await expect(
|
|
applyWorkspacePatch(
|
|
[
|
|
'*** Begin Patch',
|
|
'*** Update File: first.txt',
|
|
'@@',
|
|
'-original',
|
|
'+changed',
|
|
'*** Update File: missing.txt',
|
|
'@@',
|
|
'-missing',
|
|
'+changed',
|
|
'*** End Patch'
|
|
].join('\n'),
|
|
new LocalWorkspaceAccess(root),
|
|
new AbortController().signal
|
|
)
|
|
).rejects.toThrow('补丁目标不存在')
|
|
await expect(readFile(join(root, 'first.txt'), 'utf8')).resolves.toBe(
|
|
'original\n'
|
|
)
|
|
})
|
|
|
|
it('rejects traversal before creating files', async () => {
|
|
const root = await createWorkspace()
|
|
await expect(
|
|
applyWorkspacePatch(
|
|
'*** Begin Patch\n*** Add File: ../outside.txt\n+blocked\n*** End Patch',
|
|
new LocalWorkspaceAccess(root),
|
|
new AbortController().signal
|
|
)
|
|
).rejects.toThrow('不能超出工作区')
|
|
})
|
|
})
|