diff --git a/internal/agent/tools/view.go b/internal/agent/tools/view.go index 1b9e60546d10f5f287c8f6284c334843260f2051..9b856fd1fad961a2aabf491c0f8aac6914965e2e 100644 --- a/internal/agent/tools/view.go +++ b/internal/agent/tools/view.go @@ -272,7 +272,7 @@ func readTextFile(filePath string, offset, limit int) (string, bool, error) { // Pre-allocate slice with expected capacity. lines := make([]string, 0, limit) - for scanner.Scan() && len(lines) < limit { + for len(lines) < limit && scanner.Scan() { lineText := scanner.Text() if len(lineText) > MaxLineLength { lineText = lineText[:MaxLineLength] + "..." @@ -280,8 +280,8 @@ func readTextFile(filePath string, offset, limit int) (string, bool, error) { lines = append(lines, lineText) } - // Peek one more line to determine if the file has more content. - hasMore := scanner.Scan() + // Peek one more line only when we filled the limit. + hasMore := len(lines) == limit && scanner.Scan() if err := scanner.Err(); err != nil { return "", false, err diff --git a/internal/agent/tools/view_test.go b/internal/agent/tools/view_test.go new file mode 100644 index 0000000000000000000000000000000000000000..18b61f4e5012b4a685405fa1d1e31b90f6c790bc --- /dev/null +++ b/internal/agent/tools/view_test.go @@ -0,0 +1,87 @@ +package tools + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadTextFileBoundaryCases(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "sample.txt") + + var allLines []string + for i := range 5 { + allLines = append(allLines, fmt.Sprintf("line %d", i+1)) + } + require.NoError(t, os.WriteFile(filePath, []byte(strings.Join(allLines, "\n")), 0o644)) + + tests := []struct { + name string + offset int + limit int + wantContent string + wantHasMore bool + }{ + { + name: "exactly limit lines remaining", + offset: 0, + limit: 5, + wantContent: "line 1\nline 2\nline 3\nline 4\nline 5", + wantHasMore: false, + }, + { + name: "limit plus one line remaining", + offset: 0, + limit: 4, + wantContent: "line 1\nline 2\nline 3\nline 4", + wantHasMore: true, + }, + { + name: "offset at last line", + offset: 4, + limit: 3, + wantContent: "line 5", + wantHasMore: false, + }, + { + name: "offset beyond eof", + offset: 10, + limit: 3, + wantContent: "", + wantHasMore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotContent, gotHasMore, err := readTextFile(filePath, tt.offset, tt.limit) + require.NoError(t, err) + require.Equal(t, tt.wantContent, gotContent) + require.Equal(t, tt.wantHasMore, gotHasMore) + }) + } +} + +func TestReadTextFileTruncatesLongLines(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "longline.txt") + + longLine := strings.Repeat("a", MaxLineLength+10) + require.NoError(t, os.WriteFile(filePath, []byte(longLine), 0o644)) + + content, hasMore, err := readTextFile(filePath, 0, 1) + require.NoError(t, err) + require.False(t, hasMore) + require.Equal(t, strings.Repeat("a", MaxLineLength)+"...", content) +}