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 @@ -323,7 +323,7 @@
To read more: Use the read_file tool with offset=${nextOffset} and limit=${limit}.

${result.content}`
} else if (result.returnedLines === 0) {
} else if (result.returnedLines === 0 && result.totalLines === 0) {

Check warning on line 326 in src/core/tools/ReadFileTool.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/tools/ReadFileTool.ts:326: Survived ConditionalExpression mutant (replacement: true). See the job summary for the complete list and resolution guidance.
output = "Note: File is empty"
}

Expand Down
68 changes: 67 additions & 1 deletion src/core/tools/__tests__/readFileTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* - Output structure formatting
*/

import os from "os"
import path from "path"

import type { ToolUse } from "../../../shared/tools"
Expand Down Expand Up @@ -581,7 +582,72 @@ describe("ReadFileTool", () => {

await readFileTool.execute({ path: "empty.ts" }, mockTask as any, callbacks)

expect(callbacks.pushToolResult).toHaveBeenCalledWith(expect.stringContaining("empty"))
expect(callbacks.pushToolResult).toHaveBeenCalledWith(expect.stringContaining("Note: File is empty"))
})

it("should propagate beyond-EOF error instead of reporting file as empty", async () => {
const mockTask = createMockTask()
const callbacks = createMockCallbacks()

mockedFsReadFile.mockResolvedValue(Buffer.from("line1\nline2\nline3\nline4\nline5"))
mockedReadWithSlice.mockReturnValue({
content: "Error: offset 100 is beyond file end (5 lines)",
returnedLines: 0,
totalLines: 5,
wasTruncated: false,
includedRanges: [],
})

await readFileTool.execute(
{ path: "test.ts", mode: "slice", offset: 100 },
mockTask as unknown as Task,
callbacks,
)

expect(mockedReadWithSlice).toHaveBeenCalledWith(expect.any(String), 99, expect.any(Number))
expect(callbacks.pushToolResult).toHaveBeenCalledWith(expect.stringContaining("beyond file end"))
expect(callbacks.pushToolResult).not.toHaveBeenCalledWith(expect.stringContaining("Note: File is empty"))
})

// Integration counterpart to the mocked case above: the real readWithSlice
// producer runs against a real file, so the error text and its 1-based
// offset are proven end-to-end instead of being asserted against a stub.
it("should propagate the real beyond-EOF error from the producer for a real file", async () => {
const realFs = await vi.importActual<typeof import("fs/promises")>("fs/promises")
const realPath = await vi.importActual<typeof import("path")>("path")
const tempDir = await realFs.mkdtemp(realPath.join(os.tmpdir(), "read-file-beyond-eof-"))
const fileName = "sample.txt"
await realFs.writeFile(realPath.join(tempDir, fileName), "line1\nline2\nline3\nline4\nline5", "utf8")

vi.doUnmock("fs/promises")
vi.doUnmock("path")
vi.doUnmock("isbinaryfile")
vi.doUnmock("../../../integrations/misc/indentation-reader")
vi.resetModules()

try {
const { ReadFileTool } = await import("../ReadFileTool")
const mockTask = { ...createMockTask(), cwd: tempDir }
const callbacks = createMockCallbacks()

await new ReadFileTool().execute(
{ path: fileName, mode: "slice", offset: 100 },
mockTask as unknown as Task,
callbacks,
)

expect(mockedReadWithSlice).not.toHaveBeenCalled()
expect(callbacks.pushToolResult).toHaveBeenCalledTimes(1)
expect(callbacks.pushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Error: offset 100 is beyond file end (5 lines)"),
)
expect(callbacks.pushToolResult).not.toHaveBeenCalledWith(
expect.stringContaining("Note: File is empty"),
)
} finally {
vi.resetModules()
await realFs.rm(tempDir, { recursive: true, force: true }).catch(() => {})
}
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ describe("Unicode clipping through the existing readers", () => {

it("preserves reader errors without formatting or leaking the long line", () => {
expect(readWithSlice(longLine, 1)).toEqual({
content: "Error: offset 1 is beyond file end (1 lines)",
content: "Error: offset 2 is beyond file end (1 lines)",
includedRanges: [],
totalLines: 1,
returnedLines: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ describe("readWithSlice", () => {
const result = readWithSlice(SIMPLE_CODE, 1000, 10)

expect(result.returnedLines).toBe(0)
expect(result.content).toContain("Error")
expect(result.content).toContain("Error: offset 1001 is beyond file end")
})

it("should handle negative offset", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/misc/indentation-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export function readWithSlice(
if (offset < 0) offset = 0
if (offset >= totalLines) {
return {
content: `Error: offset ${offset} is beyond file end (${totalLines} lines)`,
content: `Error: offset ${offset + 1} is beyond file end (${totalLines} lines)`,
includedRanges: [],
totalLines,
returnedLines: 0,
Expand Down
Loading