Compare commits

...
4 Commits
2 changed files with 57 additions and 22 deletions
+14 -16
View File
@@ -112,7 +112,6 @@ export const Plugin = {
return yield* new ToolFailure({ message: "patch rejected: empty patch" })
}
const prepared: Prepared[] = []
const updates = new Map<string, string>()
const resolveTarget = Effect.fnUntraced(function* (value: string) {
const target = yield* mutation.resolve({ path: value, kind: "file" })
if (!target.externalDirectory) return target
@@ -131,6 +130,11 @@ export const Plugin = {
for (const hunk of hunks) {
yield* Effect.gen(function* () {
const target = yield* resolveTarget(hunk.path)
if (prepared.some((change) => change.target.absolute === target.absolute)) {
return yield* new ToolFailure({
message: `patch verification failed: invalid patch: multiple operations target ${target.absolute}`,
})
}
if (hunk.type === "add") {
const content =
hunk.contents.endsWith("\n") || hunk.contents === "" ? hunk.contents : `${hunk.contents}\n`
@@ -155,20 +159,15 @@ export const Plugin = {
prepared.push({ ...hunk, target, before: content.text, after: "" })
return
}
const previous = updates.get(target.absolute)
const original =
previous ??
(yield* Effect.gen(function* () {
const content = yield* FileMutation.readText(environment.files, target.absolute).pipe(
Effect.mapError(
(error) =>
new ToolFailure({
message: `patch verification failed: Failed to read file to update ${target.absolute}: ${errorMessage(error)}`,
}),
),
)
return Bom.join(content.text, content.bom)
}))
const content = yield* FileMutation.readText(environment.files, target.absolute).pipe(
Effect.mapError(
(error) =>
new ToolFailure({
message: `patch verification failed: Failed to read file to update ${target.absolute}: ${errorMessage(error)}`,
}),
),
)
const original = Bom.join(content.text, content.bom)
const before = Bom.split(original).text
const update = yield* Effect.try({
try: () => Patch.derive(hunk.path, hunk.chunks, original),
@@ -183,7 +182,6 @@ export const Plugin = {
after: update.content,
moveTarget,
})
if (!moveTarget) updates.set(target.absolute, Patch.joinBom(update.content, update.bom))
}).pipe(
Effect.mapError((error) =>
error instanceof ToolFailure
+43 -6
View File
@@ -323,6 +323,43 @@ describe("PatchTool", () => {
}),
)
it.live("rejects multiple operations on the same resolved path before writing any files", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "duplicate.txt")
yield* Effect.promise(() => fs.writeFile(target, "before\n"))
const operations = [
"*** Add File: duplicate.txt\n+after",
"*** Update File: duplicate.txt\n@@\n-before\n+after",
"*** Delete File: duplicate.txt",
]
for (const first of operations) {
for (const second of operations) {
for (const alias of ["duplicate.txt", "./duplicate.txt", target]) {
expect(
yield* executeTool(
registry,
call(
`*** Begin Patch\n*** Add File: earlier.txt\n+earlier\n${first}\n${second.replace("duplicate.txt", alias)}\n*** End Patch`,
),
),
).toEqual({
status: "error",
error: {
type: "tool.execution",
message: `patch verification failed: invalid patch: multiple operations target ${target}`,
},
})
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("before\n")
expect(yield* exists(path.join(directory, "earlier.txt"))).toBe(false)
}
}
}
expect(assertions).toEqual([])
}),
),
)
it.live("moves and updates a file", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
@@ -635,17 +672,17 @@ describe("PatchTool", () => {
),
)
it.live("applies successive update operations to one file", () =>
it.live("applies multiple chunks within one update operation", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "successive.txt")
yield* Effect.promise(() => fs.writeFile(target, "a\nb\n"))
yield* executeTool(
registry,
call(
"*** Begin Patch\n*** Update File: successive.txt\n@@\n-a\n+A\n*** Update File: successive.txt\n@@\n-b\n+B\n*** End Patch",
expect(
yield* executeTool(
registry,
call("*** Begin Patch\n*** Update File: successive.txt\n@@\n-a\n+A\n@@\n-b\n+B\n*** End Patch"),
),
)
).toMatchObject({ status: "completed" })
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("A\nB\n")
}),
),