Restore files outside the patch feature to their merge-base state: the invariant-check refactors across acp, session-controller, client, compaction, agent-loop, agent, session-title, and subprocess, the cast baseline edit, and the rescope-vendor skip list added during the merge. Rename tool-fs.enableApplyPatch to applyPatch, matching the read, write, and edit switches. Remove the unused collectMutationPaths export and the unconsumed PatchHunk.contextLineIndices field, including its SSH schema and fixtures. Report preflight refusals and syntax failures directly instead of padding every phase with outcome, affectedPaths, and cancelled fields. Add apply_patch to the shipped tool catalog test. Fold the default-activation Agent Note into the apply_patch file-editing note and delete it. Delete the guarded-filesystem-mutations upgrade guide: every change is additive and all FileSystem subclasses live in this repository. Trim defensive and reviewer-addressed sentences from the package READMEs, subsystem docs, FileSystem JSDoc, and the Agent Note, keeping the English and Chinese pairs aligned.
51 lines
2.7 KiB
TypeScript
51 lines
2.7 KiB
TypeScript
/** Resolution failures preserve operation order and use raw spelling only for unresolved paths. */
|
|
import { FsError } from '@deepseek-ai/dsh-fs'
|
|
import { expect, it, vi } from 'vitest'
|
|
import { add, move, patch, setup, update } from './harness.ts'
|
|
|
|
it('collects path resolution failures while retaining resolved paths and checking remaining operations', async () => {
|
|
const t = await setup()
|
|
await t.file('a', 'old\n')
|
|
const resolve = t.fs.resolve.bind(t.fs)
|
|
vi.spyOn(t.fs, 'resolve').mockImplementation(async (path, options) => {
|
|
if (path === './denied') throw new FsError('cannot resolve denied path', 'FS_PERMISSION_DENIED')
|
|
return resolve(path, options)
|
|
})
|
|
expect(await t.run(patch(update('a'), add('./denied'), add('c')))).toEqual({ phase: 'preflight', refusals: [
|
|
{ operation: { kind: 'add', path: './denied', index: 1, patchLine: 6 }, failure: { code: 'FS_PERMISSION_DENIED', message: 'cannot resolve denied path' } },
|
|
] })
|
|
expect(await t.bytes('a')).toBe('old\n')
|
|
await expect(t.bytes('c')).rejects.toMatchObject({ code: 'ENOENT' })
|
|
expect(t.observations).toEqual([])
|
|
expect(await t.editIntent('a')).toEqual({ version: await t.version('a') })
|
|
})
|
|
|
|
it('retains a resolved move source when only its destination cannot be resolved', async () => {
|
|
const t = await setup()
|
|
await t.file('a', 'old\n')
|
|
const resolve = t.fs.resolve.bind(t.fs)
|
|
vi.spyOn(t.fs, 'resolve').mockImplementation(async (path, options) => {
|
|
if (path === './denied') throw new FsError('destination unread', 'FS_NOT_OBSERVED')
|
|
return resolve(path, options)
|
|
})
|
|
expect(await t.run(patch(move('a', './denied'), update('a')))).toEqual({ phase: 'preflight', refusals: [
|
|
{ operation: { ...t.record('move', 'a'), destination: './denied' },
|
|
failure: { code: 'FS_NOT_OBSERVED', message: 'cannot modify "./denied": file has not been read — read the file, then retry' } },
|
|
] })
|
|
expect(await t.bytes('a')).toBe('old\n')
|
|
expect(t.observations).toEqual([])
|
|
expect(await t.editIntent('a')).toEqual({ version: await t.version('a') })
|
|
})
|
|
|
|
it('returns aborted resolution refusals for a signal aborted before entry', async () => {
|
|
const t = await setup()
|
|
t.controller.abort()
|
|
expect(await t.run(patch(add('a'), move('b', 'c')))).toEqual({ phase: 'preflight', refusals: [
|
|
{ operation: { kind: 'add', path: 'a', index: 0, patchLine: 2 }, failure: { code: 'FS_ABORTED', message: 'resolve aborted' } },
|
|
{ operation: { kind: 'move', path: 'b', destination: 'c', index: 1, patchLine: 4 }, failure: { code: 'FS_ABORTED', message: 'resolve aborted' } },
|
|
] })
|
|
await expect(t.bytes('a')).rejects.toMatchObject({ code: 'ENOENT' })
|
|
expect(t.observations).toEqual([])
|
|
await expect(t.editIntent('a')).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
|
|
})
|