@@ -902,6 +902,157 @@ globalThis._evals.functions.push({
902902 ) ;
903903}
904904
905+ #[ test]
906+ fn functions_js_runner_reexecutes_imported_input_files ( ) {
907+ if !command_exists ( "node" ) {
908+ eprintln ! (
909+ "Skipping functions_js_runner_reexecutes_imported_input_files (node not installed)."
910+ ) ;
911+ return ;
912+ }
913+ let Some ( tsc) = find_tsc ( ) else {
914+ eprintln ! (
915+ "Skipping functions_js_runner_reexecutes_imported_input_files (tsc not installed)."
916+ ) ;
917+ return ;
918+ } ;
919+
920+ let root = repo_root ( ) ;
921+ let tmp = tempdir ( ) . expect ( "tempdir" ) ;
922+ let sample_b_path = tmp. path ( ) . join ( "sample-b.mjs" ) ;
923+ std:: fs:: write (
924+ & sample_b_path,
925+ r#"globalThis._evals ??= { functions: [], prompts: [], parameters: [], evaluators: {}, reporters: {} };
926+ globalThis._evals.functions.push({
927+ name: "js-tool-b",
928+ slug: "js-tool-b",
929+ type: "tool",
930+ preview: "export function b() { return 2; }"
931+ });
932+ export const b = 2;
933+ "# ,
934+ )
935+ . expect ( "write sample-b.mjs" ) ;
936+
937+ let sample_a_path = tmp. path ( ) . join ( "sample-a.mjs" ) ;
938+ std:: fs:: write (
939+ & sample_a_path,
940+ r#"import "./sample-b.mjs";
941+ globalThis._evals ??= { functions: [], prompts: [], parameters: [], evaluators: {}, reporters: {} };
942+ globalThis._evals.functions.push({
943+ name: "js-tool-a",
944+ slug: "js-tool-a",
945+ type: "tool",
946+ preview: "export function a() { return 1; }"
947+ });
948+ "# ,
949+ )
950+ . expect ( "write sample-a.mjs" ) ;
951+
952+ let runner_dir = tmp. path ( ) . join ( "runner" ) ;
953+ let compile_output = Command :: new ( & tsc)
954+ . current_dir ( & root)
955+ . args ( [
956+ "scripts/functions-runner.ts" ,
957+ "scripts/runner-common.ts" ,
958+ "--module" ,
959+ "esnext" ,
960+ "--target" ,
961+ "es2020" ,
962+ "--moduleResolution" ,
963+ "bundler" ,
964+ "--outDir" ,
965+ ] )
966+ . arg ( & runner_dir)
967+ . output ( )
968+ . expect ( "compile functions runner" ) ;
969+ if !compile_output. status . success ( ) {
970+ let stderr = String :: from_utf8_lossy ( & compile_output. stderr ) ;
971+ panic ! ( "tsc failed for functions runner:\n {stderr}" ) ;
972+ }
973+
974+ let runner_js = runner_dir. join ( "functions-runner.js" ) ;
975+ let runner_common_js = runner_dir. join ( "runner-common.js" ) ;
976+ assert ! ( runner_js. is_file( ) , "compiled functions-runner.js missing" ) ;
977+ assert ! (
978+ runner_common_js. is_file( ) ,
979+ "compiled runner-common.js missing"
980+ ) ;
981+
982+ let runner_code = std:: fs:: read_to_string ( & runner_js) . expect ( "read compiled runner" ) ;
983+ let patched_runner_code = runner_code
984+ . replace ( "\" ./runner-common\" " , "\" ./runner-common.js\" " )
985+ . replace ( "'./runner-common'" , "'./runner-common.js'" ) ;
986+ assert_ne ! (
987+ runner_code, patched_runner_code,
988+ "compiled runner import path did not contain ./runner-common"
989+ ) ;
990+ std:: fs:: write ( & runner_js, patched_runner_code) . expect ( "write patched compiled runner" ) ;
991+ std:: fs:: write ( runner_dir. join ( "package.json" ) , r#"{ "type": "module" }"# )
992+ . expect ( "write runner package.json" ) ;
993+
994+ let output = Command :: new ( "node" )
995+ . arg ( & runner_js)
996+ . arg ( & sample_a_path)
997+ . arg ( & sample_b_path)
998+ . output ( )
999+ . expect ( "run compiled functions runner" ) ;
1000+ if !output. status . success ( ) {
1001+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
1002+ panic ! ( "compiled functions runner failed:\n {stderr}" ) ;
1003+ }
1004+
1005+ let manifest: Value = serde_json:: from_slice ( & output. stdout ) . expect ( "parse manifest JSON" ) ;
1006+ let files = manifest[ "files" ] . as_array ( ) . expect ( "files array" ) ;
1007+ assert_eq ! ( files. len( ) , 2 , "expected two manifest files" ) ;
1008+
1009+ let sample_a_canonical = sample_a_path
1010+ . canonicalize ( )
1011+ . expect ( "canonicalize sample-a.mjs" ) ;
1012+ let sample_b_canonical = sample_b_path
1013+ . canonicalize ( )
1014+ . expect ( "canonicalize sample-b.mjs" ) ;
1015+ let mut files_by_source = BTreeMap :: new ( ) ;
1016+ for file in files {
1017+ let source_file = file
1018+ . get ( "source_file" )
1019+ . and_then ( Value :: as_str)
1020+ . expect ( "source_file" ) ;
1021+ let canonical_source = PathBuf :: from ( source_file)
1022+ . canonicalize ( )
1023+ . expect ( "canonicalize manifest source_file" ) ;
1024+ files_by_source. insert ( canonical_source, file) ;
1025+ }
1026+
1027+ let file_a = files_by_source
1028+ . get ( & sample_a_canonical)
1029+ . expect ( "manifest file for sample-a.mjs" ) ;
1030+ let entries_a = file_a
1031+ . get ( "entries" )
1032+ . and_then ( Value :: as_array)
1033+ . expect ( "sample-a entries" ) ;
1034+ assert ! (
1035+ entries_a
1036+ . iter( )
1037+ . any( |entry| { entry. get( "slug" ) . and_then( Value :: as_str) == Some ( "js-tool-a" ) } ) ,
1038+ "expected sample-a.mjs entries to include js-tool-a"
1039+ ) ;
1040+
1041+ let file_b = files_by_source
1042+ . get ( & sample_b_canonical)
1043+ . expect ( "manifest file for sample-b.mjs" ) ;
1044+ let entries_b = file_b
1045+ . get ( "entries" )
1046+ . and_then ( Value :: as_array)
1047+ . expect ( "sample-b entries" ) ;
1048+ assert ! (
1049+ entries_b
1050+ . iter( )
1051+ . any( |entry| { entry. get( "slug" ) . and_then( Value :: as_str) == Some ( "js-tool-b" ) } ) ,
1052+ "expected sample-b.mjs entries to include js-tool-b"
1053+ ) ;
1054+ }
1055+
9051056#[ test]
9061057fn functions_python_runner_emits_valid_manifest_with_bundle ( ) {
9071058 let Some ( python) = find_python ( ) else {
0 commit comments