From 83df51a567adee202163a0730c6b7ee1e27960c4 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 18:06:31 +0000 Subject: [PATCH 1/4] fix: respect maxReadFileLine setting for file mentions to prevent context exhaustion - Modified extractTextFromFile to accept and respect maxReadFileLine parameter - Updated parseMentions and related functions to pass maxReadFileLine through the call chain - Modified Task.ts to retrieve maxReadFileLine from state and pass to processUserContentMentions - Added comprehensive tests for large file handling - Files are now truncated with informative messages when they exceed the line limit Fixes #6069 --- src/core/mentions/index.ts | 14 +- .../mentions/processUserContentMentions.ts | 5 + src/core/task/Task.ts | 2 + .../extract-text-large-files.spec.ts | 224 ++++++++++++++++++ src/integrations/misc/extract-text.ts | 18 +- 5 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 src/integrations/misc/__tests__/extract-text-large-files.spec.ts diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 7ce54b984e1..d0d305d0965 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -82,6 +82,7 @@ export async function parseMentions( showRooIgnoredFiles: boolean = true, includeDiagnosticMessages: boolean = true, maxDiagnosticMessages: number = 50, + maxReadFileLine?: number, ): Promise { const mentions: Set = new Set() let parsedText = text.replace(mentionRegexGlobal, (match, mention) => { @@ -149,7 +150,13 @@ export async function parseMentions( } else if (mention.startsWith("/")) { const mentionPath = mention.slice(1) try { - const content = await getFileOrFolderContent(mentionPath, cwd, rooIgnoreController, showRooIgnoredFiles) + const content = await getFileOrFolderContent( + mentionPath, + cwd, + rooIgnoreController, + showRooIgnoredFiles, + maxReadFileLine, + ) if (mention.endsWith("/")) { parsedText += `\n\n\n${content}\n` } else { @@ -212,6 +219,7 @@ async function getFileOrFolderContent( cwd: string, rooIgnoreController?: any, showRooIgnoredFiles: boolean = true, + maxReadFileLine?: number, ): Promise { const unescapedPath = unescapeSpaces(mentionPath) const absPath = path.resolve(cwd, unescapedPath) @@ -224,7 +232,7 @@ async function getFileOrFolderContent( return `(File ${mentionPath} is ignored by .rooignore)` } try { - const content = await extractTextFromFile(absPath) + const content = await extractTextFromFile(absPath, maxReadFileLine) return content } catch (error) { return `(Failed to read contents of ${mentionPath}): ${error.message}` @@ -264,7 +272,7 @@ async function getFileOrFolderContent( if (isBinary) { return undefined } - const content = await extractTextFromFile(absoluteFilePath) + const content = await extractTextFromFile(absoluteFilePath, maxReadFileLine) return `\n${content}\n` } catch (error) { return undefined diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index 0649c4bc3c3..245a25b3793 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -15,6 +15,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles = true, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, + maxReadFileLine, }: { userContent: Anthropic.Messages.ContentBlockParam[] cwd: string @@ -24,6 +25,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles?: boolean includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number + maxReadFileLine?: number }) { // Process userContent array, which contains various block types: // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. @@ -52,6 +54,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } @@ -71,6 +74,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } @@ -91,6 +95,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 95d12f66aa1..fe8fd0f68fe 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1230,6 +1230,7 @@ export class Task extends EventEmitter { showRooIgnoredFiles = true, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, + maxReadFileLine = -1, } = (await this.providerRef.deref()?.getState()) ?? {} const parsedUserContent = await processUserContentMentions({ @@ -1241,6 +1242,7 @@ export class Task extends EventEmitter { showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, }) const environmentDetails = await getEnvironmentDetails(this, includeFileDetails) diff --git a/src/integrations/misc/__tests__/extract-text-large-files.spec.ts b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts new file mode 100644 index 00000000000..14b361c4ba2 --- /dev/null +++ b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts @@ -0,0 +1,224 @@ +// npx vitest run integrations/misc/__tests__/extract-text-large-files.spec.ts + +import { describe, it, expect, vi, beforeEach, Mock } from "vitest" +import * as fs from "fs/promises" +import { extractTextFromFile } from "../extract-text" +import { countFileLines } from "../line-counter" +import { readLines } from "../read-lines" +import { isBinaryFile } from "isbinaryfile" + +// Mock all dependencies +vi.mock("fs/promises") +vi.mock("../line-counter") +vi.mock("../read-lines") +vi.mock("isbinaryfile") + +describe("extractTextFromFile - Large File Handling", () => { + // Type the mocks + const mockedFs = vi.mocked(fs) + const mockedCountFileLines = vi.mocked(countFileLines) + const mockedReadLines = vi.mocked(readLines) + const mockedIsBinaryFile = vi.mocked(isBinaryFile) + + beforeEach(() => { + vi.clearAllMocks() + // Set default mock behavior + mockedFs.access.mockResolvedValue(undefined) + mockedIsBinaryFile.mockResolvedValue(false) + }) + + it("should truncate files that exceed maxReadFileLine limit", async () => { + const largeFileContent = Array(150) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line with some content`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(150) + mockedReadLines.mockResolvedValue( + Array(100) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line with some content`) + .join("\n"), + ) + + const result = await extractTextFromFile("/test/large-file.ts", 100) + + // Should only include first 100 lines with line numbers + expect(result).toContain(" 1 | Line 1: This is a test line with some content") + expect(result).toContain("100 | Line 100: This is a test line with some content") + expect(result).not.toContain("101 | Line 101: This is a test line with some content") + + // Should include truncation message + expect(result).toContain( + "[File truncated: showing 100 of 150 total lines. The file is too large and may exhaust the context window if read in full.]", + ) + }) + + it("should not truncate files within the maxReadFileLine limit", async () => { + const smallFileContent = Array(50) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(50) + mockedFs.readFile.mockResolvedValue(smallFileContent as any) + + const result = await extractTextFromFile("/test/small-file.ts", 100) + + // Should include all lines with line numbers + expect(result).toContain(" 1 | Line 1: This is a test line") + expect(result).toContain("50 | Line 50: This is a test line") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle files with exactly maxReadFileLine lines", async () => { + const exactFileContent = Array(100) + .fill(null) + .map((_, i) => `Line ${i + 1}`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(100) + mockedFs.readFile.mockResolvedValue(exactFileContent as any) + + const result = await extractTextFromFile("/test/exact-file.ts", 100) + + // Should include all lines with line numbers + expect(result).toContain(" 1 | Line 1") + expect(result).toContain("100 | Line 100") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle undefined maxReadFileLine by not truncating", async () => { + const largeFileContent = Array(200) + .fill(null) + .map((_, i) => `Line ${i + 1}`) + .join("\n") + + mockedFs.readFile.mockResolvedValue(largeFileContent as any) + + const result = await extractTextFromFile("/test/large-file.ts", undefined) + + // Should include all lines with line numbers when maxReadFileLine is undefined + expect(result).toContain(" 1 | Line 1") + expect(result).toContain("200 | Line 200") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle empty files", async () => { + mockedFs.readFile.mockResolvedValue("" as any) + + const result = await extractTextFromFile("/test/empty-file.ts", 100) + + expect(result).toBe("") + expect(result).not.toContain("[File truncated:") + }) + + it("should handle files with only newlines", async () => { + const newlineOnlyContent = "\n\n\n\n\n" + + mockedCountFileLines.mockResolvedValue(6) // 5 newlines = 6 lines + mockedReadLines.mockResolvedValue("\n\n") + + const result = await extractTextFromFile("/test/newline-file.ts", 3) + + // Should truncate at line 3 + expect(result).toContain("[File truncated: showing 3 of 6 total lines") + }) + + it("should handle very large files efficiently", async () => { + // Simulate a 10,000 line file + mockedCountFileLines.mockResolvedValue(10000) + mockedReadLines.mockResolvedValue( + Array(500) + .fill(null) + .map((_, i) => `Line ${i + 1}: Some content here`) + .join("\n"), + ) + + const result = await extractTextFromFile("/test/very-large-file.ts", 500) + + // Should only include first 500 lines with line numbers + expect(result).toContain(" 1 | Line 1: Some content here") + expect(result).toContain("500 | Line 500: Some content here") + expect(result).not.toContain("501 | Line 501: Some content here") + + // Should show truncation message + expect(result).toContain("[File truncated: showing 500 of 10000 total lines") + }) + + it("should handle maxReadFileLine of 0 by not truncating", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + + mockedFs.readFile.mockResolvedValue(fileContent as any) + + const result = await extractTextFromFile("/test/file.ts", 0) + + // maxReadFileLine of 0 or negative means no limit + expect(result).toContain("1 | Line 1") + expect(result).toContain("2 | Line 2") + expect(result).toContain("3 | Line 3") + expect(result).not.toContain("[File truncated:") + }) + + it("should handle negative maxReadFileLine by treating as undefined", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + + mockedFs.readFile.mockResolvedValue(fileContent as any) + + const result = await extractTextFromFile("/test/file.ts", -1) + + // Should include all content with line numbers when negative + expect(result).toContain("1 | Line 1") + expect(result).toContain("2 | Line 2") + expect(result).toContain("3 | Line 3") + expect(result).not.toContain("[File truncated:") + }) + + it("should preserve file content structure when truncating", async () => { + const structuredContent = [ + "function example() {", + " const x = 1;", + " const y = 2;", + " return x + y;", + "}", + "", + "// More code below", + ].join("\n") + + mockedCountFileLines.mockResolvedValue(7) + mockedReadLines.mockResolvedValue(["function example() {", " const x = 1;", " const y = 2;"].join("\n")) + + const result = await extractTextFromFile("/test/structured.ts", 3) + + // Should preserve the first 3 lines with line numbers + expect(result).toContain("1 | function example() {") + expect(result).toContain("2 | const x = 1;") + expect(result).toContain("3 | const y = 2;") + expect(result).not.toContain("4 | return x + y;") + + // Should include truncation info + expect(result).toContain("[File truncated: showing 3 of 7 total lines") + }) + + it("should handle binary files by throwing an error", async () => { + mockedIsBinaryFile.mockResolvedValue(true) + + await expect(extractTextFromFile("/test/binary.bin", 100)).rejects.toThrow( + "Cannot read text for file type: .bin", + ) + }) + + it("should handle file not found errors", async () => { + mockedFs.access.mockRejectedValue(new Error("ENOENT")) + + await expect(extractTextFromFile("/test/nonexistent.ts", 100)).rejects.toThrow( + "File not found: /test/nonexistent.ts", + ) + }) +}) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index eb02f63b952..477b30a6f21 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -5,6 +5,8 @@ import mammoth from "mammoth" import fs from "fs/promises" import { isBinaryFile } from "isbinaryfile" import { extractTextFromXLSX } from "./extract-text-from-xlsx" +import { countFileLines } from "./line-counter" +import { readLines } from "./read-lines" async function extractTextFromPDF(filePath: string): Promise { const dataBuffer = await fs.readFile(filePath) @@ -48,7 +50,7 @@ export function getSupportedBinaryFormats(): string[] { return Object.keys(SUPPORTED_BINARY_FORMATS) } -export async function extractTextFromFile(filePath: string): Promise { +export async function extractTextFromFile(filePath: string, maxReadFileLine?: number): Promise { try { await fs.access(filePath) } catch (error) { @@ -67,6 +69,20 @@ export async function extractTextFromFile(filePath: string): Promise { const isBinary = await isBinaryFile(filePath).catch(() => false) if (!isBinary) { + // Check if we need to apply line limit + if (maxReadFileLine && maxReadFileLine > 0) { + const totalLines = await countFileLines(filePath) + if (totalLines > maxReadFileLine) { + // Read only up to maxReadFileLine + const content = await readLines(filePath, maxReadFileLine - 1, 0) + const numberedContent = addLineNumbers(content) + return ( + numberedContent + + `\n\n[File truncated: showing ${maxReadFileLine} of ${totalLines} total lines. The file is too large and may exhaust the context window if read in full.]` + ) + } + } + // Read the entire file if no limit or file is within limit return addLineNumbers(await fs.readFile(filePath, "utf8")) } else { throw new Error(`Cannot read text for file type: ${fileExtension}`) From 37776ba2bb49c7fb97102441aa01e3b14146553d Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 25 Jul 2025 12:07:08 -0500 Subject: [PATCH 2/4] feat: enhance extractTextFromFile to support line limit validation and documentation --- .../processUserContentMentions.spec.ts | 343 ++++++++++++++++++ src/integrations/misc/extract-text.ts | 24 +- 2 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 src/core/mentions/__tests__/processUserContentMentions.spec.ts diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts new file mode 100644 index 00000000000..e0355698eae --- /dev/null +++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts @@ -0,0 +1,343 @@ +// npx vitest core/mentions/__tests__/processUserContentMentions.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import { processUserContentMentions } from "../processUserContentMentions" +import { parseMentions } from "../index" +import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher" +import { FileContextTracker } from "../../context-tracking/FileContextTracker" + +// Mock the parseMentions function +vi.mock("../index", () => ({ + parseMentions: vi.fn(), +})) + +describe("processUserContentMentions", () => { + let mockUrlContentFetcher: UrlContentFetcher + let mockFileContextTracker: FileContextTracker + let mockRooIgnoreController: any + + beforeEach(() => { + vi.clearAllMocks() + + mockUrlContentFetcher = {} as UrlContentFetcher + mockFileContextTracker = {} as FileContextTracker + mockRooIgnoreController = {} + + // Default mock implementation + vi.mocked(parseMentions).mockImplementation(async (text) => `parsed: ${text}`) + }) + + describe("maxReadFileLine parameter", () => { + it("should pass maxReadFileLine to parseMentions when provided", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read file with limit", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + maxReadFileLine: 100, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read file with limit", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + 100, + ) + }) + + it("should pass undefined maxReadFileLine when not provided", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read file without limit", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read file without limit", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + undefined, + ) + }) + + it("should handle UNLIMITED_LINES constant correctly", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read unlimited lines", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + maxReadFileLine: -1, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read unlimited lines", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + -1, + ) + }) + }) + + describe("content processing", () => { + it("should process text blocks with tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Do something", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "text", + text: "parsed: Do something", + }) + }) + + it("should process text blocks with tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Fix this issue", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "text", + text: "parsed: Fix this issue", + }) + }) + + it("should not process text blocks without task or feedback tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Regular text without special tags", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).not.toHaveBeenCalled() + expect(result[0]).toEqual(userContent[0]) + }) + + it("should process tool_result blocks with string content", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "123", + content: "Tool feedback", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "123", + content: "parsed: Tool feedback", + }) + }) + + it("should process tool_result blocks with array content", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "123", + content: [ + { + type: "text" as const, + text: "Array task", + }, + { + type: "text" as const, + text: "Regular text", + }, + ], + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalledTimes(1) + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "123", + content: [ + { + type: "text", + text: "parsed: Array task", + }, + { + type: "text", + text: "Regular text", + }, + ], + }) + }) + + it("should handle mixed content types", async () => { + const userContent = [ + { + type: "text" as const, + text: "First task", + }, + { + type: "image" as const, + source: { + type: "base64" as const, + media_type: "image/png" as const, + data: "base64data", + }, + }, + { + type: "tool_result" as const, + tool_use_id: "456", + content: "Feedback", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + maxReadFileLine: 50, + }) + + expect(parseMentions).toHaveBeenCalledTimes(2) + expect(result).toHaveLength(3) + expect(result[0]).toEqual({ + type: "text", + text: "parsed: First task", + }) + expect(result[1]).toEqual(userContent[1]) // Image block unchanged + expect(result[2]).toEqual({ + type: "tool_result", + tool_use_id: "456", + content: "parsed: Feedback", + }) + }) + }) + + describe("showRooIgnoredFiles parameter", () => { + it("should default showRooIgnoredFiles to true", async () => { + const userContent = [ + { + type: "text" as const, + text: "Test default", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Test default", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + undefined, + true, // showRooIgnoredFiles should default to true + undefined, + ) + }) + + it("should respect showRooIgnoredFiles when explicitly set to false", async () => { + const userContent = [ + { + type: "text" as const, + text: "Test explicit false", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + showRooIgnoredFiles: false, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Test explicit false", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + undefined, + false, + undefined, + ) + }) + }) +}) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 477b30a6f21..8231c609be7 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -50,7 +50,27 @@ export function getSupportedBinaryFormats(): string[] { return Object.keys(SUPPORTED_BINARY_FORMATS) } +/** + * Extracts text content from a file, with support for various formats including PDF, DOCX, XLSX, and plain text. + * For large text files, can limit the number of lines read to prevent context exhaustion. + * + * @param filePath - Path to the file to extract text from + * @param maxReadFileLine - Maximum number of lines to read from text files. + * Use UNLIMITED_LINES (-1) or undefined for no limit. + * Must be a positive integer or UNLIMITED_LINES. + * @returns Promise resolving to the extracted text content with line numbers + * @throws {Error} If file not found, unsupported format, or invalid parameters + */ export async function extractTextFromFile(filePath: string, maxReadFileLine?: number): Promise { + // Validate maxReadFileLine parameter + if (maxReadFileLine !== undefined && maxReadFileLine !== -1) { + if (!Number.isInteger(maxReadFileLine) || maxReadFileLine < 1) { + throw new Error( + `Invalid maxReadFileLine: ${maxReadFileLine}. Must be a positive integer or -1 for unlimited.`, + ) + } + } + try { await fs.access(filePath) } catch (error) { @@ -70,10 +90,10 @@ export async function extractTextFromFile(filePath: string, maxReadFileLine?: nu if (!isBinary) { // Check if we need to apply line limit - if (maxReadFileLine && maxReadFileLine > 0) { + if (maxReadFileLine !== undefined && maxReadFileLine !== -1) { const totalLines = await countFileLines(filePath) if (totalLines > maxReadFileLine) { - // Read only up to maxReadFileLine + // Read only up to maxReadFileLine (endLine is 0-based and inclusive) const content = await readLines(filePath, maxReadFileLine - 1, 0) const numberedContent = addLineNumbers(content) return ( From 65823f047a326547c0a3396063052b73a230f000 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 25 Jul 2025 12:15:45 -0500 Subject: [PATCH 3/4] fix: type assertion for bedrock token config --- src/api/providers/bedrock.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 76e502e6c7d..ee27367e19d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -224,8 +224,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (this.options.awsUseApiKey && this.options.awsApiKey) { // Use API key/token-based authentication if enabled and API key is set - clientConfig.token = { token: this.options.awsApiKey } - clientConfig.authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. + ;(clientConfig as any).token = { token: this.options.awsApiKey } + ;(clientConfig as any).authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. } else if (this.options.awsUseProfile && this.options.awsProfile) { // Use profile-based credentials if enabled and profile is set clientConfig.credentials = fromIni({ From 95dfbad4354a0ca910649a88c0d5f4df767109fa Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 25 Jul 2025 12:28:34 -0500 Subject: [PATCH 4/4] fix: update tests to match new function signatures after merge --- src/api/providers/bedrock.ts | 4 ++-- .../__tests__/processUserContentMentions.spec.ts | 10 ++++++++++ .../misc/__tests__/extract-text-large-files.spec.ts | 13 +++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index ee27367e19d..76e502e6c7d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -224,8 +224,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (this.options.awsUseApiKey && this.options.awsApiKey) { // Use API key/token-based authentication if enabled and API key is set - ;(clientConfig as any).token = { token: this.options.awsApiKey } - ;(clientConfig as any).authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. + clientConfig.token = { token: this.options.awsApiKey } + clientConfig.authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. } else if (this.options.awsUseProfile && this.options.awsProfile) { // Use profile-based credentials if enabled and profile is set clientConfig.credentials = fromIni({ diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts index e0355698eae..3aebd66e53b 100644 --- a/src/core/mentions/__tests__/processUserContentMentions.spec.ts +++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts @@ -52,6 +52,8 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages 100, ) }) @@ -79,6 +81,8 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages undefined, ) }) @@ -107,6 +111,8 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages -1, ) }) @@ -309,6 +315,8 @@ describe("processUserContentMentions", () => { mockFileContextTracker, undefined, true, // showRooIgnoredFiles should default to true + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages undefined, ) }) @@ -336,6 +344,8 @@ describe("processUserContentMentions", () => { mockFileContextTracker, undefined, false, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages undefined, ) }) diff --git a/src/integrations/misc/__tests__/extract-text-large-files.spec.ts b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts index 14b361c4ba2..fc2f7f54b6b 100644 --- a/src/integrations/misc/__tests__/extract-text-large-files.spec.ts +++ b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts @@ -152,18 +152,15 @@ describe("extractTextFromFile - Large File Handling", () => { expect(result).toContain("[File truncated: showing 500 of 10000 total lines") }) - it("should handle maxReadFileLine of 0 by not truncating", async () => { + it("should handle maxReadFileLine of 0 by throwing an error", async () => { const fileContent = "Line 1\nLine 2\nLine 3" mockedFs.readFile.mockResolvedValue(fileContent as any) - const result = await extractTextFromFile("/test/file.ts", 0) - - // maxReadFileLine of 0 or negative means no limit - expect(result).toContain("1 | Line 1") - expect(result).toContain("2 | Line 2") - expect(result).toContain("3 | Line 3") - expect(result).not.toContain("[File truncated:") + // maxReadFileLine of 0 should throw an error + await expect(extractTextFromFile("/test/file.ts", 0)).rejects.toThrow( + "Invalid maxReadFileLine: 0. Must be a positive integer or -1 for unlimited.", + ) }) it("should handle negative maxReadFileLine by treating as undefined", async () => {