@@ -1185,6 +1185,78 @@ fn search_match_file_dir_and_scan_caps_match_native() {
11851185 assert_exact_envelope ( & native, & rss. result ) ;
11861186}
11871187
1188+ fn write_search_files ( fixture : & Fixture , relative_paths : & [ & str ] ) {
1189+ for name in relative_paths {
1190+ let path = fixture. root . join ( name) ;
1191+ if let Some ( parent) = path. parent ( ) {
1192+ fs:: create_dir_all ( parent) . unwrap ( ) ;
1193+ }
1194+ fs:: write ( path, "needle\n " ) . unwrap ( ) ;
1195+ }
1196+ }
1197+
1198+ fn assert_search_exam_budget_eq (
1199+ label : & str ,
1200+ max_search_files : usize ,
1201+ files : & [ & str ] ,
1202+ arguments : Value ,
1203+ ) {
1204+ let fixture = Fixture :: new ( label) ;
1205+ write_search_files ( & fixture, files) ;
1206+ let mut config = fixture. config ( ) ;
1207+ config. max_search_files = max_search_files;
1208+ config. artifact_store . root = fixture. parent . join ( format ! ( "artifacts-{label}" ) ) ;
1209+ let native = native_execute (
1210+ & fixture. tools_with_config ( config. clone ( ) ) ,
1211+ NativeToolExecutor :: SearchFiles ,
1212+ & arguments,
1213+ ) ;
1214+ let rss = run_rss_search ( & fixture, & config, arguments) ;
1215+ assert_exact_envelope ( & native, & rss. result ) ;
1216+ }
1217+
1218+ #[ test]
1219+ fn search_enumeration_budget_counts_dot_slots_like_native ( ) {
1220+ let content = json ! ( { "pattern" : "needle" } ) ;
1221+ let files_target = json ! ( { "pattern" : "*.txt" , "target" : "files" } ) ;
1222+
1223+ // Budget N with N-1 and N real entries: native counts `.` and `..` first.
1224+ assert_search_exam_budget_eq (
1225+ "exam-n-minus-1" ,
1226+ 4 ,
1227+ & [ "a.txt" , "b.txt" , "c.txt" ] ,
1228+ content. clone ( ) ,
1229+ ) ;
1230+ assert_search_exam_budget_eq (
1231+ "exam-n" ,
1232+ 4 ,
1233+ & [ "a.txt" , "b.txt" , "c.txt" , "d.txt" ] ,
1234+ content. clone ( ) ,
1235+ ) ;
1236+ assert_search_exam_budget_eq ( "exam-n-pass" , 4 , & [ "a.txt" , "b.txt" ] , content. clone ( ) ) ;
1237+ assert_search_exam_budget_eq (
1238+ "exam-n-files-target" ,
1239+ 4 ,
1240+ & [ "a.txt" , "b.txt" , "c.txt" ] ,
1241+ files_target. clone ( ) ,
1242+ ) ;
1243+
1244+ // remaining <= 2 at the search root, including empty directories.
1245+ assert_search_exam_budget_eq ( "exam-rem-1-file" , 1 , & [ "a.txt" ] , content. clone ( ) ) ;
1246+ assert_search_exam_budget_eq ( "exam-rem-1-empty" , 1 , & [ ] , content. clone ( ) ) ;
1247+ assert_search_exam_budget_eq ( "exam-rem-2-one" , 2 , & [ "a.txt" ] , content. clone ( ) ) ;
1248+ assert_search_exam_budget_eq ( "exam-rem-2-two" , 2 , & [ "a.txt" , "b.txt" ] , content. clone ( ) ) ;
1249+ assert_search_exam_budget_eq ( "exam-rem-2-empty" , 2 , & [ ] , content. clone ( ) ) ;
1250+
1251+ // Sibling directory entered with remaining == 2 after a prior tree consumed files.
1252+ assert_search_exam_budget_eq (
1253+ "exam-nested-rem-2" ,
1254+ 5 ,
1255+ & [ "adir/f0.txt" , "adir/f1.txt" , "adir/f2.txt" , "zdir/late.txt" ] ,
1256+ content,
1257+ ) ;
1258+ }
1259+
11881260fn assert_policy_denied_before_prepare (
11891261 module : & ' static str ,
11901262 tool_name : & ' static str ,
@@ -1230,6 +1302,7 @@ fn read_path_policy_is_rejected_before_prepare() {
12301302 json ! ( { "path" : "notes.txt." } ) ,
12311303 json ! ( { "path" : "notes.txt" , "offset" : 0 } ) ,
12321304 json ! ( { "path" : "notes.txt" , "offset" : -1 } ) ,
1305+ json ! ( { "path" : "你" . repeat( 100 ) } ) ,
12331306 ] {
12341307 assert_policy_denied_before_prepare (
12351308 "read_file.rss" ,
@@ -1255,6 +1328,7 @@ fn search_path_policy_is_rejected_before_prepare() {
12551328 json ! ( { "pattern" : "alpha" , "path" : "a:b" } ) ,
12561329 json ! ( { "pattern" : "alpha" , "path" : "a\\ b" } ) ,
12571330 json ! ( { "pattern" : "alpha" , "offset" : -1 } ) ,
1331+ json ! ( { "pattern" : "alpha" , "path" : "你" . repeat( 100 ) } ) ,
12581332 ] {
12591333 assert_policy_denied_before_prepare (
12601334 "search_files.rss" ,
@@ -1266,6 +1340,30 @@ fn search_path_policy_is_rejected_before_prepare() {
12661340 }
12671341}
12681342
1343+ #[ test]
1344+ fn cjk_component_byte_limit_is_rejected_before_prepare_like_native ( ) {
1345+ let fixture = Fixture :: new ( "cjk-component-bytes" ) ;
1346+ fs:: write ( fixture. root . join ( "notes.txt" ) , "alpha\n " ) . unwrap ( ) ;
1347+ let component = "你" . repeat ( 100 ) ;
1348+ assert_eq ! ( component. len( ) , 300 ) ;
1349+ assert_eq ! ( component. chars( ) . count( ) , 100 ) ;
1350+ assert ! ( component. chars( ) . count( ) < 255 ) ;
1351+ assert_policy_denied_before_prepare (
1352+ "read_file.rss" ,
1353+ "read_file" ,
1354+ NativeToolExecutor :: ReadFile ,
1355+ & fixture,
1356+ json ! ( { "path" : component. clone( ) } ) ,
1357+ ) ;
1358+ assert_policy_denied_before_prepare (
1359+ "search_files.rss" ,
1360+ "search_files" ,
1361+ NativeToolExecutor :: SearchFiles ,
1362+ & fixture,
1363+ json ! ( { "pattern" : "alpha" , "path" : component} ) ,
1364+ ) ;
1365+ }
1366+
12691367#[ test]
12701368fn search_one_nanosecond_budget_ceils_to_one_ms_and_matches_native ( ) {
12711369 assert_eq ! ( positive_duration_ms( Duration :: ZERO ) , 0 ) ;
@@ -1286,7 +1384,7 @@ fn search_one_nanosecond_budget_ceils_to_one_ms_and_matches_native() {
12861384
12871385 let arguments = json ! ( { "pattern" : "alpha" } ) ;
12881386 let native = native_execute (
1289- & fixture. tools ( ) ,
1387+ & fixture. tools_with_config ( config . clone ( ) ) ,
12901388 NativeToolExecutor :: SearchFiles ,
12911389 & arguments,
12921390 ) ;
@@ -1300,7 +1398,7 @@ fn search_one_nanosecond_budget_ceils_to_one_ms_and_matches_native() {
13001398 durable : MemoryDurable :: new ( ) ,
13011399 approval : Arc :: new ( AllowAll ) ,
13021400 cancellation : Arc :: new ( NeverCancelled ) ,
1303- clock : JumpClock :: new ( 1_000 , u64 :: MAX , 1_000 ) ,
1401+ clock : JumpClock :: new ( 1_000 , 1 , 1_001 ) ,
13041402 deadline_ms : 1_000_000 ,
13051403 install_artifacts : false ,
13061404 artifact_limits : default_artifact_limits ( ) ,
@@ -1309,7 +1407,6 @@ fn search_one_nanosecond_budget_ceils_to_one_ms_and_matches_native() {
13091407 ) ;
13101408 assert_exact_envelope ( & native, & rss. result ) ;
13111409 assert_eq ! ( rss. result[ "ok" ] , json!( true ) , "rss={}" , rss. result) ;
1312- assert_eq ! ( rss. result[ "truncated" ] , json!( false ) , "rss={}" , rss. result) ;
13131410 assert ! ( rss. started > 0 ) ;
13141411}
13151412
@@ -1689,9 +1786,10 @@ fn search_glob_question_mark_matches_one_utf8_byte_like_native() {
16891786 let fixture = Fixture :: new ( "glob-byte-q" ) ;
16901787 fs:: write ( fixture. root . join ( "a.rs" ) , "keep\n " ) . unwrap ( ) ;
16911788 fs:: write ( fixture. root . join ( "你.rs" ) , "cjk\n " ) . unwrap ( ) ;
1692- assert_search_eq ( & fixture, json ! ( { "target " : "files " , "file_glob " : "?.rs " } ) ) ;
1693- assert_search_eq ( & fixture, json ! ( { "target " : "files " , "file_glob " : "???.rs " } ) ) ;
1789+ assert_search_eq ( & fixture, json ! ( { "pattern " : "?.rs " , "target " : "files " } ) ) ;
1790+ assert_search_eq ( & fixture, json ! ( { "pattern " : "???.rs " , "target " : "files " } ) ) ;
16941791 assert_search_eq ( & fixture, json ! ( { "pattern" : "keep" , "file_glob" : "?.rs" } ) ) ;
1792+ assert_search_eq ( & fixture, json ! ( { "pattern" : "cjk" , "file_glob" : "???.rs" } ) ) ;
16951793}
16961794
16971795#[ cfg( unix) ]
@@ -1707,7 +1805,7 @@ fn search_skips_non_utf8_names_like_native() {
17071805 . join ( OsString :: from_vec ( vec ! [ 0xff , b'x' , 0x80 ] ) ) ;
17081806 fs:: write ( & bad, "secret alpha\n " ) . unwrap ( ) ;
17091807 assert_search_eq ( & fixture, json ! ( { "pattern" : "alpha" } ) ) ;
1710- assert_search_eq ( & fixture, json ! ( { "target " : "files " , "file_glob " : "* " } ) ) ;
1808+ assert_search_eq ( & fixture, json ! ( { "pattern " : "* " , "target " : "files " } ) ) ;
17111809}
17121810
17131811#[ test]
@@ -1717,7 +1815,7 @@ fn search_directory_order_is_byte_lexicographic_including_multibyte() {
17171815 fs:: write ( fixture. root . join ( name) , "needle\n " ) . unwrap ( ) ;
17181816 }
17191817 assert_search_eq ( & fixture, json ! ( { "pattern" : "needle" } ) ) ;
1720- assert_search_eq ( & fixture, json ! ( { "target" : "files" } ) ) ;
1818+ assert_search_eq ( & fixture, json ! ( { "pattern" : "*" , " target": "files" } ) ) ;
17211819}
17221820
17231821#[ test]
@@ -1727,7 +1825,7 @@ fn search_high_entry_directory_order_matches_native() {
17271825 fs:: write ( fixture. root . join ( format ! ( "f-{i:03}.txt" ) ) , "needle\n " ) . unwrap ( ) ;
17281826 }
17291827 assert_search_eq ( & fixture, json ! ( { "pattern" : "needle" } ) ) ;
1730- assert_search_eq ( & fixture, json ! ( { "target" : "files" } ) ) ;
1828+ assert_search_eq ( & fixture, json ! ( { "pattern" : "*" , " target": "files" } ) ) ;
17311829}
17321830
17331831#[ test]
0 commit comments