@@ -2159,6 +2159,103 @@ fn search_hardlink_in_child_discards_parent_matches_like_native() {
21592159 ) ;
21602160}
21612161
2162+ #[ cfg( unix) ]
2163+ #[ test]
2164+ fn search_remaining_2_hardlink_only_truncates_like_native ( ) {
2165+ let fixture = Fixture :: new ( "search-rem2-hardlink" ) ;
2166+ let outside = fixture. parent . join ( "outside-shared-rem2" ) ;
2167+ fs:: write ( & outside, "needle shared\n " ) . unwrap ( ) ;
2168+ fs:: hard_link ( & outside, fixture. root . join ( "linked" ) ) . unwrap ( ) ;
2169+ let mut config = fixture. config ( ) ;
2170+ config. max_search_files = 2 ;
2171+ config. artifact_store . root = fixture. parent . join ( "artifacts-rem2-hardlink" ) ;
2172+ let arguments = json ! ( { "pattern" : "needle" } ) ;
2173+ let native = native_execute (
2174+ & fixture. tools_with_config ( config. clone ( ) ) ,
2175+ NativeToolExecutor :: SearchFiles ,
2176+ & arguments,
2177+ ) ;
2178+ let rss = run_rss_search ( & fixture, & config, arguments) ;
2179+ assert_exact_envelope ( & native, & rss. result ) ;
2180+ assert ! ( native. ok, "native={native:?}" ) ;
2181+ assert ! ( native. truncated, "native must truncate: {native:?}" ) ;
2182+ assert_eq ! ( native. content, "" ) ;
2183+ assert_eq ! ( native. data[ "match_count" ] , json!( 0 ) ) ;
2184+ assert_eq ! ( native. data[ "files_visited" ] , json!( 0 ) ) ;
2185+ assert_eq ! ( native. data[ "dirs_visited" ] , json!( 1 ) ) ;
2186+ assert_eq ! ( rss. result[ "ok" ] , json!( true ) , "rss={}" , rss. result) ;
2187+ assert_eq ! ( rss. result[ "truncated" ] , json!( true ) , "rss={}" , rss. result) ;
2188+ assert_eq ! ( rss. result[ "content" ] , json!( "" ) ) ;
2189+ assert_eq ! ( rss. result[ "data" ] [ "match_count" ] , json!( 0 ) ) ;
2190+ assert_eq ! ( rss. result[ "data" ] [ "files_visited" ] , json!( 0 ) ) ;
2191+ assert_eq ! ( rss. result[ "data" ] [ "dirs_visited" ] , json!( 1 ) ) ;
2192+ }
2193+
2194+ #[ cfg( unix) ]
2195+ #[ test]
2196+ fn search_nested_remaining_2_hardlink_sibling_stops_later_siblings_like_native ( ) {
2197+ let fixture = Fixture :: new ( "search-nested-rem2-hardlink" ) ;
2198+ fs:: create_dir_all ( fixture. root . join ( "adir" ) ) . unwrap ( ) ;
2199+ fs:: create_dir_all ( fixture. root . join ( "bdir" ) ) . unwrap ( ) ;
2200+ fs:: create_dir_all ( fixture. root . join ( "zdir" ) ) . unwrap ( ) ;
2201+ fs:: write ( fixture. root . join ( "adir/f0.txt" ) , "needle\n " ) . unwrap ( ) ;
2202+ fs:: write ( fixture. root . join ( "adir/f1.txt" ) , "needle\n " ) . unwrap ( ) ;
2203+ fs:: write ( fixture. root . join ( "adir/f2.txt" ) , "needle\n " ) . unwrap ( ) ;
2204+ let outside = fixture. parent . join ( "outside-shared-nested" ) ;
2205+ fs:: write ( & outside, "secret needle\n " ) . unwrap ( ) ;
2206+ fs:: hard_link ( & outside, fixture. root . join ( "bdir/linked" ) ) . unwrap ( ) ;
2207+ fs:: write ( fixture. root . join ( "zdir/late.txt" ) , "needle late\n " ) . unwrap ( ) ;
2208+ let mut config = fixture. config ( ) ;
2209+ config. max_search_files = 5 ;
2210+ config. artifact_store . root = fixture. parent . join ( "artifacts-nested-rem2-hardlink" ) ;
2211+ let arguments = json ! ( { "pattern" : "needle" } ) ;
2212+ let native = native_execute (
2213+ & fixture. tools_with_config ( config. clone ( ) ) ,
2214+ NativeToolExecutor :: SearchFiles ,
2215+ & arguments,
2216+ ) ;
2217+ let rss = run_rss_search ( & fixture, & config, arguments) ;
2218+ assert_exact_envelope ( & native, & rss. result ) ;
2219+ assert ! ( native. ok, "native={native:?}" ) ;
2220+ assert ! ( native. truncated, "native must truncate: {native:?}" ) ;
2221+ assert ! (
2222+ native. content. contains( "adir/f0.txt" )
2223+ && native. content. contains( "adir/f1.txt" )
2224+ && native. content. contains( "adir/f2.txt" ) ,
2225+ "prior matches must remain: native={native:?}"
2226+ ) ;
2227+ assert ! (
2228+ !native. content. contains( "late.txt" ) ,
2229+ "later sibling must not be traversed after remaining<=2 hardlink: native={native:?}"
2230+ ) ;
2231+ assert ! (
2232+ !native. content. contains( "linked" ) ,
2233+ "hardlink must not be searched after remaining<=2: native={native:?}"
2234+ ) ;
2235+ assert_eq ! ( native. data[ "files_visited" ] , json!( 3 ) ) ;
2236+ assert_eq ! ( native. data[ "dirs_visited" ] , json!( 3 ) ) ;
2237+ assert_eq ! ( rss. result[ "ok" ] , json!( true ) , "rss={}" , rss. result) ;
2238+ assert_eq ! ( rss. result[ "truncated" ] , json!( true ) , "rss={}" , rss. result) ;
2239+ assert_eq ! ( rss. result[ "data" ] [ "files_visited" ] , json!( 3 ) ) ;
2240+ assert_eq ! ( rss. result[ "data" ] [ "dirs_visited" ] , json!( 3 ) ) ;
2241+ assert ! (
2242+ rss. result[ "content" ]
2243+ . as_str( )
2244+ . is_some_and( |content| content. contains( "adir/f0.txt" )
2245+ && content. contains( "adir/f1.txt" )
2246+ && content. contains( "adir/f2.txt" ) ) ,
2247+ "prior matches must remain: rss={}" ,
2248+ rss. result
2249+ ) ;
2250+ assert ! (
2251+ !rss. result[ "content" ]
2252+ . as_str( )
2253+ . is_some_and( |content| content. contains( "late.txt" ) || content. contains( "linked" ) ) ,
2254+ "later sibling and hardlink must not leak: rss={}" ,
2255+ rss. result
2256+ ) ;
2257+ }
2258+
21622259#[ test]
21632260fn search_directory_order_is_byte_lexicographic_including_multibyte ( ) {
21642261 let fixture = Fixture :: new ( "sort-multi" ) ;
0 commit comments