Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/tools/ReadFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ export class ReadFileTool extends BaseTool<"read_file"> {
const fullPath = filePath ? path.resolve(task.cwd, filePath) : ""
const sharedMessageProps: ClineSayTool = {
tool: "readFile",
path: getReadablePath(task.cwd, filePath),
path: filePath ? getReadablePath(task.cwd, filePath) : "",
isOutsideWorkspace: filePath ? isPathOutsideWorkspace(fullPath) : false,
}
const partialMessage = JSON.stringify({
Expand Down
86 changes: 76 additions & 10 deletions src/core/tools/__tests__/readFileTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

import path from "path"

import type { ToolUse } from "../../../shared/tools"
import type { Task } from "../../task/Task"

import { isBinaryFile } from "isbinaryfile"

import { readFileTool, ReadFileTool } from "../ReadFileTool"
Expand Down Expand Up @@ -987,19 +990,82 @@ describe("ReadFileTool", () => {
})

describe("handlePartial", () => {
it("should handle partial display for new format", async () => {
it.each([{}, { path: "" }, undefined])(
"renders streamed native arguments before their path arrives: %j",
async (nativeArgs) => {
const mockTask = createMockTask()
mockTask.cwd = path.resolve(mockTask.cwd)
const streamedBlock = {
type: "tool_use",
name: "read_file",
params: {},
nativeArgs,
partial: true,
}

// Streaming JSON is incomplete even though finalized native arguments require a path.
await Reflect.apply(readFileTool.handlePartial, readFileTool, [mockTask, streamedBlock])

expect(mockTask.ask).toHaveBeenCalledExactlyOnceWith(
"tool",
JSON.stringify({ tool: "readFile", path: "", isOutsideWorkspace: false }),
true,
)
expect(mockedFsReadFile).not.toHaveBeenCalled()
expect(mockedFsStat).not.toHaveBeenCalled()
expect(mockedExtractTextFromFile).not.toHaveBeenCalled()
},
)

it("renders incomplete legacy arguments without a file entry or outside-workspace warning", async () => {
const mockTask = createMockTask()
mockTask.ask.mockResolvedValue({ response: "yesButtonClicked", text: undefined, images: undefined })

const block = {
nativeArgs: { path: "src/app.ts" },
mockTask.cwd = path.resolve(mockTask.cwd)
const block: ToolUse<"read_file"> = {
type: "tool_use",
name: "read_file",
params: {},
nativeArgs: { files: [], _legacyFormat: true },
partial: true,
}

await readFileTool.handlePartial(mockTask as any, block as any)
// This reader-boundary double omits Task fields unrelated to file reading.
await readFileTool.handlePartial(mockTask as unknown as Task, block)

expect(mockTask.ask).toHaveBeenCalledWith("tool", expect.stringContaining("readFile"), true)
})
expect(mockTask.ask).toHaveBeenCalledExactlyOnceWith(
"tool",
JSON.stringify({ tool: "readFile", path: "", isOutsideWorkspace: false }),
true,
)
expect(mockedFsReadFile).not.toHaveBeenCalled()
expect(mockedFsStat).not.toHaveBeenCalled()
expect(mockedExtractTextFromFile).not.toHaveBeenCalled()
})

it.each(["src/app.ts", "src/файл.ts", "src/e\u0301-📚.ts"])(
"should preserve partial display for %s",
async (filePath) => {
const mockTask = createMockTask()
mockTask.cwd = path.resolve(mockTask.cwd)
mockTask.ask.mockResolvedValue({ response: "yesButtonClicked", text: undefined, images: undefined })

const block: ToolUse<"read_file"> = {
type: "tool_use",
name: "read_file",
params: {},
nativeArgs: { path: filePath },
partial: true,
}

// This reader-boundary double omits Task fields unrelated to file reading.
await readFileTool.handlePartial(mockTask as unknown as Task, block)

expect(mockTask.ask).toHaveBeenCalledExactlyOnceWith(
"tool",
JSON.stringify({ tool: "readFile", path: filePath, isOutsideWorkspace: true }),
true,
)
},
)

it("should handle partial display for legacy format", async () => {
const mockTask = createMockTask()
Expand Down Expand Up @@ -1043,9 +1109,9 @@ describe("ReadFileTool", () => {
expect(mockTask.ask).toHaveBeenCalled()
})

it("should gracefully handle ask rejection in partial", async () => {
it.each(["Cancelled", "superseded"])("should gracefully handle %s ask rejection in partial", async (reason) => {
const mockTask = createMockTask()
mockTask.ask.mockRejectedValue(new Error("Cancelled"))
mockTask.ask.mockRejectedValue(new Error(reason))

const block = {
nativeArgs: { path: "test.ts" },
Expand Down
2 changes: 1 addition & 1 deletion src/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@
},
"core/tools/__tests__/readFileTool.spec.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 98
"count": 96
}
},
"core/tools/__tests__/runSlashCommandTool.spec.ts": {
Expand Down
Loading