fix: create parent directories early in write_to_file to prevent ENOENT errors (#9640)

This commit is contained in:
Daniel 2025-11-26 19:53:10 -05:00 committed by GitHub
parent 867a5c79f9
commit 4cdec7db8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 5 deletions

View file

@ -7,7 +7,7 @@ import { Task } from "../task/Task"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { formatResponse } from "../prompts/responses"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { fileExistsAtPath } from "../../utils/fs"
import { fileExistsAtPath, createDirectoriesForFile } from "../../utils/fs"
import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
@ -70,15 +70,21 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false
let fileExists: boolean
const absolutePath = path.resolve(task.cwd, relPath)
if (task.diffViewProvider.editType !== undefined) {
fileExists = task.diffViewProvider.editType === "modify"
} else {
const absolutePath = path.resolve(task.cwd, relPath)
fileExists = await fileExistsAtPath(absolutePath)
task.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// Create parent directories early for new files to prevent ENOENT errors
// in subsequent operations (e.g., diffViewProvider.open, fs.readFile)
if (!fileExists) {
await createDirectoriesForFile(absolutePath)
}
if (newContent.startsWith("```")) {
newContent = newContent.split("\n").slice(1).join("\n")
}
@ -307,16 +313,23 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
}
let fileExists: boolean
const absolutePath = path.resolve(task.cwd, relPath)
if (task.diffViewProvider.editType !== undefined) {
fileExists = task.diffViewProvider.editType === "modify"
} else {
const absolutePath = path.resolve(task.cwd, relPath)
fileExists = await fileExistsAtPath(absolutePath)
task.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// Create parent directories early for new files to prevent ENOENT errors
// in subsequent operations (e.g., diffViewProvider.open)
if (!fileExists) {
await createDirectoriesForFile(absolutePath)
}
const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false
const fullPath = path.resolve(task.cwd, relPath)
const fullPath = absolutePath
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
const sharedMessageProps: ClineSayTool = {

View file

@ -2,7 +2,7 @@ import * as path from "path"
import type { MockedFunction } from "vitest"
import { fileExistsAtPath } from "../../../utils/fs"
import { fileExistsAtPath, createDirectoriesForFile } from "../../../utils/fs"
import { detectCodeOmission } from "../../../integrations/editor/detect-omission"
import { isPathOutsideWorkspace } from "../../../utils/pathUtils"
import { getReadablePath } from "../../../utils/path"
@ -29,6 +29,7 @@ vi.mock("delay", () => ({
vi.mock("../../../utils/fs", () => ({
fileExistsAtPath: vi.fn().mockResolvedValue(false),
createDirectoriesForFile: vi.fn().mockResolvedValue([]),
}))
vi.mock("../../prompts/responses", () => ({
@ -101,6 +102,7 @@ describe("writeToFileTool", () => {
// Mocked functions with correct types
const mockedFileExistsAtPath = fileExistsAtPath as MockedFunction<typeof fileExistsAtPath>
const mockedCreateDirectoriesForFile = createDirectoriesForFile as MockedFunction<typeof createDirectoriesForFile>
const mockedDetectCodeOmission = detectCodeOmission as MockedFunction<typeof detectCodeOmission>
const mockedIsPathOutsideWorkspace = isPathOutsideWorkspace as MockedFunction<typeof isPathOutsideWorkspace>
const mockedGetReadablePath = getReadablePath as MockedFunction<typeof getReadablePath>
@ -276,6 +278,48 @@ describe("writeToFileTool", () => {
})
})
describe("directory creation for new files", () => {
it.skipIf(process.platform === "win32")(
"creates parent directories early when file does not exist (execute)",
async () => {
await executeWriteFileTool({}, { fileExists: false })
expect(mockedCreateDirectoriesForFile).toHaveBeenCalledWith(absoluteFilePath)
},
)
it.skipIf(process.platform === "win32")(
"creates parent directories early when file does not exist (partial)",
async () => {
await executeWriteFileTool({}, { fileExists: false, isPartial: true })
expect(mockedCreateDirectoriesForFile).toHaveBeenCalledWith(absoluteFilePath)
},
)
it("does not create directories when file exists", async () => {
await executeWriteFileTool({}, { fileExists: true })
expect(mockedCreateDirectoriesForFile).not.toHaveBeenCalled()
})
it("does not create directories when editType is cached as modify", async () => {
mockCline.diffViewProvider.editType = "modify"
await executeWriteFileTool({})
expect(mockedCreateDirectoriesForFile).not.toHaveBeenCalled()
})
it.skipIf(process.platform === "win32")("creates directories when editType is cached as create", async () => {
mockCline.diffViewProvider.editType = "create"
await executeWriteFileTool({})
expect(mockedCreateDirectoriesForFile).toHaveBeenCalledWith(absoluteFilePath)
})
})
describe("content preprocessing", () => {
it("removes markdown code block markers from content", async () => {
await executeWriteFileTool({ content: testContentWithMarkdown })