@@ -53,6 +53,19 @@ impl GrepOutputMode {
5353 }
5454}
5555
56+ fn has_non_neutral_page_controls ( args : & serde_json:: Value ) -> bool {
57+ let limit_is_non_neutral = match args. get ( "limit" ) {
58+ None | Some ( serde_json:: Value :: Null ) => false ,
59+ Some ( value) => value. as_u64 ( ) != Some ( DEFAULT_PAGE_LIMIT as u64 ) ,
60+ } ;
61+ let cursor_is_non_neutral = match args. get ( "cursor" ) {
62+ None | Some ( serde_json:: Value :: Null ) => false ,
63+ Some ( serde_json:: Value :: String ( value) ) => !value. trim ( ) . is_empty ( ) ,
64+ Some ( _) => true ,
65+ } ;
66+ limit_is_non_neutral || cursor_is_non_neutral
67+ }
68+
5669pub struct GrepTool ;
5770
5871#[ async_trait]
@@ -150,9 +163,13 @@ impl Tool for GrepTool {
150163 Err ( error) => return Ok ( ToolOutput :: error ( error) ) ,
151164 }
152165 } else {
153- if args. get ( "limit" ) . is_some ( ) || args. get ( "cursor" ) . is_some ( ) {
166+ // Structured-output providers can materialize omitted optional
167+ // fields with their neutral schema defaults. Do not reject a
168+ // content/summary call merely because it carries limit=200 and an
169+ // empty cursor; neither value changes the non-paginated result.
170+ if has_non_neutral_page_controls ( args) {
154171 return Ok ( ToolOutput :: error (
155- "limit and cursor are supported only with output_mode='files_with_matches' or 'count'" ,
172+ "non-default limit and non-empty cursor are supported only with output_mode='files_with_matches' or 'count'" ,
156173 ) ) ;
157174 }
158175 None
@@ -735,6 +752,49 @@ mod tests {
735752 assert_eq ! ( result. metadata. unwrap( ) [ "search" ] [ "truncated" ] , false ) ;
736753 }
737754
755+ #[ tokio:: test]
756+ async fn test_grep_non_paginated_mode_accepts_materialized_neutral_page_defaults ( ) {
757+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
758+ std:: fs:: write ( temp. path ( ) . join ( "a.txt" ) , "needle\n " ) . unwrap ( ) ;
759+ let ctx = ToolContext :: new ( temp. path ( ) . to_path_buf ( ) ) ;
760+
761+ let result = GrepTool
762+ . execute (
763+ & serde_json:: json!( {
764+ "pattern" : "needle" ,
765+ "output_mode" : "content" ,
766+ "limit" : DEFAULT_PAGE_LIMIT ,
767+ "cursor" : ""
768+ } ) ,
769+ & ctx,
770+ )
771+ . await
772+ . unwrap ( ) ;
773+
774+ assert ! ( result. success, "{}" , result. content) ;
775+ assert ! ( result. content. contains( "needle" ) ) ;
776+ }
777+
778+ #[ tokio:: test]
779+ async fn test_grep_non_paginated_mode_rejects_effective_page_controls ( ) {
780+ let ctx = ToolContext :: new ( PathBuf :: from ( "/tmp" ) ) ;
781+ for controls in [
782+ serde_json:: json!( { "limit" : 2 } ) ,
783+ serde_json:: json!( { "cursor" : "2" } ) ,
784+ ] {
785+ let mut args = serde_json:: json!( {
786+ "pattern" : "needle" ,
787+ "output_mode" : "summary"
788+ } ) ;
789+ args. as_object_mut ( )
790+ . unwrap ( )
791+ . extend ( controls. as_object ( ) . unwrap ( ) . clone ( ) ) ;
792+ let result = GrepTool . execute ( & args, & ctx) . await . unwrap ( ) ;
793+ assert ! ( !result. success) ;
794+ assert ! ( result. content. contains( "supported only" ) ) ;
795+ }
796+ }
797+
738798 #[ cfg( unix) ]
739799 #[ tokio:: test]
740800 async fn test_grep_source_anchor_preserves_newline_filename_without_injection ( ) {
0 commit comments