diff --git a/src/tools/file_read.rs b/src/tools/file_read.rs index 4d6463a..f170d8e 100644 --- a/src/tools/file_read.rs +++ b/src/tools/file_read.rs @@ -3,6 +3,7 @@ use std::path::Path; use async_trait::async_trait; use serde_json::json; +use crate::text::take_prefix_chars; use crate::tools::traits::{Tool, ToolResult}; use crate::tools::extract_u64; @@ -187,8 +188,13 @@ impl Tool for FileReadTool { } end_idx = i + 1; } - result = lines[..end_idx].join("\n"); - let truncated_amount = original_len - result.len(); + if end_idx == 0 && !lines.is_empty() { + // First line alone exceeds MAX_CHARS — take its prefix + result = take_prefix_chars(&lines[0], MAX_CHARS.saturating_sub(100)); + } else { + result = lines[..end_idx].join("\n"); + } + let truncated_amount = original_len.saturating_sub(result.len()); result.push_str(&format!( "\n\n... ({} chars truncated) ...", truncated_amount @@ -312,4 +318,28 @@ mod tests { assert!(!result.success); assert!(result.error.unwrap().contains("Not a file")); } + + #[tokio::test] + async fn test_read_single_long_line() { + let mut file = NamedTempFile::new().unwrap(); + // Write a single line longer than MAX_CHARS + let long_line = "A".repeat(150_000); + file.write_all(long_line.as_bytes()).unwrap(); + + let tool = FileReadTool::new(); + let result = tool + .execute(json!({ "path": file.path().to_str().unwrap() })) + .await + .unwrap(); + + assert!(result.success); + // Should contain the line number prefix and the beginning of the content + assert!(result.output.starts_with("1| AAAA")); + // Should contain truncation notice since content exceeds MAX_CHARS + assert!(result.output.contains("chars truncated")); + // Should contain end-of-file notice (1 line total) + assert!(result.output.contains("End of file — 1 lines total")); + // Should NOT be empty content — the fix ensures the prefix is preserved + assert!(result.output.len() > 100); + } }