diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index de9749121..fc8ab7784 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -536,13 +536,13 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T } limit := int64(-1) - if raw, exists := args["limit"]; exists && raw != nil { - limit, err = getInt64Arg(args, "limit", -1) + if raw, exists := args["max_lines"]; exists && raw != nil { + limit, err = getInt64Arg(args, "max_lines", -1) if err != nil { return ErrorResult(err.Error()) } if limit <= 0 { - return ErrorResult("limit, if provided, must be > 0") + return ErrorResult("max_lines, if provided, must be > 0") } } @@ -630,7 +630,7 @@ func (t *ReadFileLinesTool) Execute(ctx context.Context, args map[string]any) *T } if linesRead == 0 && content.Len() == 0 { - return NewToolResult("[END OF FILE - no content at this offset]") + return NewToolResult(fmt.Sprintf("[END OF FILE - no content at or after start_line=%d]", startLine)) } start := startLine diff --git a/pkg/tools/filesystem_test.go b/pkg/tools/filesystem_test.go index 14b34b604..49842f82e 100644 --- a/pkg/tools/filesystem_test.go +++ b/pkg/tools/filesystem_test.go @@ -883,7 +883,7 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { result1 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, - "limit": 2, + "max_lines": 2, }) if result1.IsError { t.Fatalf("Chunk 1 failed: %s", result1.ForLLM) @@ -901,7 +901,7 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { result2 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 3, - "limit": 2, + "max_lines": 2, }) if result2.IsError { t.Fatalf("Chunk 2 failed: %s", result2.ForLLM) @@ -916,7 +916,7 @@ func TestReadFileLinesTool_ChunkedReading(t *testing.T) { result3 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 5, - "limit": 2, + "max_lines": 2, }) if result3.IsError { t.Fatalf("Chunk 3 failed: %s", result3.ForLLM) @@ -1000,11 +1000,71 @@ func TestReadFileLinesTool_OffsetBeyondEOF(t *testing.T) { if result.IsError { t.Fatalf("unexpected error: %s", result.ForLLM) } - if result.ForLLM != "[END OF FILE - no content at this offset]" { + if result.ForLLM != "[END OF FILE - no content at or after start_line=100]" { t.Fatalf("unexpected EOF message: %q", result.ForLLM) } } +func TestReadFileLinesTool_RegistryValidationSupportsMaxLinesAndRejectsLimit(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "registry_lines.txt") + + err := os.WriteFile(testFile, []byte("line 1\nline 2\nline 3\n"), 0o644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + reg := NewToolRegistry() + reg.Register(NewReadFileLinesTool(tmpDir, false, MaxReadFileSize)) + + result := reg.Execute(context.Background(), "read_file", map[string]any{ + "path": testFile, + "start_line": 1, + "max_lines": 1, + }) + if result.IsError { + t.Fatalf("expected max_lines to pass registry validation, got: %s", result.ForLLM) + } + if !strings.Contains(result.ForLLM, "1|line 1\n") { + t.Fatalf("expected first line via max_lines, got: %s", result.ForLLM) + } + + result = reg.Execute(context.Background(), "read_file", map[string]any{ + "path": testFile, + "start_line": 2, + "limit": 1, + }) + if !result.IsError { + t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM) + } + if !strings.Contains(result.ForLLM, "unexpected property \"limit\"") { + t.Fatalf("expected registry validation error for limit, got: %s", result.ForLLM) + } +} + +func TestReadFileLinesTool_RejectsLegacyLimit(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "legacy_limit.txt") + + err := os.WriteFile(testFile, []byte("line 1\nline 2\n"), 0o644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) + result := tool.Execute(context.Background(), map[string]any{ + "path": testFile, + "start_line": 1, + "limit": 1, + }) + if !result.IsError { + t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM) + } + if !strings.Contains(result.ForLLM, "limit is no longer supported; use max_lines") { + t.Fatalf("unexpected error for legacy limit: %s", result.ForLLM) + } +} + func TestReadFileLinesTool_BinaryFileRejected(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "binary.dat")