diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index cf558ad634..e4e69b30ad 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -323,7 +323,7 @@ export class ReadFileTool extends BaseTool<"read_file"> { 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) { output = "Note: File is empty" } diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 2d6e9ecc0d..52e04415ea 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -12,6 +12,7 @@ * - Output structure formatting */ +import os from "os" import path from "path" import type { ToolUse } from "../../../shared/tools" @@ -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("fs/promises") + const realPath = await vi.importActual("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(() => {}) + } }) }) diff --git a/src/integrations/misc/__tests__/indentation-reader-unicode.spec.ts b/src/integrations/misc/__tests__/indentation-reader-unicode.spec.ts index 90d5007f16..bfc397cf09 100644 --- a/src/integrations/misc/__tests__/indentation-reader-unicode.spec.ts +++ b/src/integrations/misc/__tests__/indentation-reader-unicode.spec.ts @@ -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, diff --git a/src/integrations/misc/__tests__/indentation-reader.spec.ts b/src/integrations/misc/__tests__/indentation-reader.spec.ts index b6c28365aa..a092dff8fc 100644 --- a/src/integrations/misc/__tests__/indentation-reader.spec.ts +++ b/src/integrations/misc/__tests__/indentation-reader.spec.ts @@ -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", () => { diff --git a/src/integrations/misc/indentation-reader.ts b/src/integrations/misc/indentation-reader.ts index 47ff4593bf..43e86a47ad 100644 --- a/src/integrations/misc/indentation-reader.ts +++ b/src/integrations/misc/indentation-reader.ts @@ -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,