@@ -526,6 +526,122 @@ mod tests {
526526 assert_eq ! ( cmd. as_std( ) . get_program( ) , OsStr :: new( expected_program) ) ;
527527 }
528528
529+ // ---- check_runtime_compatibility ----
530+
531+ fn make_runtime_with_version ( version : & str ) -> JsRuntime {
532+ let binary_path = if cfg ! ( windows) {
533+ AbsolutePathBuf :: new ( "C:\\ node\\ node.exe" . into ( ) ) . unwrap ( )
534+ } else {
535+ AbsolutePathBuf :: new ( "/usr/local/bin/node" . into ( ) ) . unwrap ( )
536+ } ;
537+ let mut runtime = JsRuntime :: from_system ( JsRuntimeType :: Node , binary_path) ;
538+ runtime. version = version. into ( ) ;
539+ runtime
540+ }
541+
542+ #[ test]
543+ fn compatible_version_passes ( ) {
544+ let runtime = make_runtime_with_version ( "22.12.0" ) ;
545+ assert ! (
546+ check_runtime_compatibility( & runtime, "^20.19.0 || >=22.12.0" ) . is_ok( ) ,
547+ "22.12.0 should satisfy ^20.19.0 || >=22.12.0"
548+ ) ;
549+ }
550+
551+ #[ test]
552+ fn compatible_version_with_v_prefix_passes ( ) {
553+ let runtime = make_runtime_with_version ( "v20.19.0" ) ;
554+ assert ! (
555+ check_runtime_compatibility( & runtime, "^20.19.0 || >=22.12.0" ) . is_ok( ) ,
556+ "v20.19.0 should satisfy ^20.19.0 || >=22.12.0"
557+ ) ;
558+ }
559+
560+ #[ test]
561+ fn incompatible_version_returns_error ( ) {
562+ let runtime = make_runtime_with_version ( "18.0.0" ) ;
563+ let err = check_runtime_compatibility ( & runtime, "^20.19.0 || >=22.12.0" )
564+ . expect_err ( "18.0.0 should not satisfy ^20.19.0 || >=22.12.0" ) ;
565+ assert ! (
566+ matches!( err, Error :: NodeVersionIncompatible { .. } ) ,
567+ "expected NodeVersionIncompatible, got {err:?}"
568+ ) ;
569+ }
570+
571+ #[ test]
572+ fn system_runtime_skips_check ( ) {
573+ // from_system() sets version == "system"
574+ let binary_path = if cfg ! ( windows) {
575+ AbsolutePathBuf :: new ( "C:\\ node\\ node.exe" . into ( ) ) . unwrap ( )
576+ } else {
577+ AbsolutePathBuf :: new ( "/usr/local/bin/node" . into ( ) ) . unwrap ( )
578+ } ;
579+ let runtime = JsRuntime :: from_system ( JsRuntimeType :: Node , binary_path) ;
580+ assert_eq ! ( runtime. version( ) , "system" ) ;
581+ assert ! (
582+ check_runtime_compatibility( & runtime, "^20.19.0 || >=22.12.0" ) . is_ok( ) ,
583+ "system runtime should pass the check unconditionally"
584+ ) ;
585+ }
586+
587+ #[ test]
588+ fn unparseable_version_skips_check ( ) {
589+ let runtime = make_runtime_with_version ( "not-a-version" ) ;
590+ assert ! (
591+ check_runtime_compatibility( & runtime, "^20.19.0 || >=22.12.0" ) . is_ok( ) ,
592+ "unparseable version should not cause an error"
593+ ) ;
594+ }
595+
596+ #[ test]
597+ fn invalid_requirement_skips_check ( ) {
598+ let runtime = make_runtime_with_version ( "18.0.0" ) ;
599+ assert ! (
600+ check_runtime_compatibility( & runtime, "not-a-range" ) . is_ok( ) ,
601+ "invalid requirement range should not cause an error"
602+ ) ;
603+ }
604+
605+ // ---- get_cli_engines_requirement ----
606+
607+ #[ tokio:: test]
608+ async fn get_cli_engines_requirement_reads_from_package_json ( ) {
609+ use std:: io:: Write ;
610+
611+ use tempfile:: TempDir ;
612+
613+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
614+ // The scripts dir is `<pkg_dir>/dist`, and get_cli_package_dir returns its parent.
615+ let dist_dir = temp_dir. path ( ) . join ( "dist" ) ;
616+ std:: fs:: create_dir ( & dist_dir) . unwrap ( ) ;
617+
618+ let pkg_json_content = r#"{ "engines": { "node": "^20.19.0 || >=22.12.0" } }"# ;
619+ let mut f = std:: fs:: File :: create ( temp_dir. path ( ) . join ( "package.json" ) ) . unwrap ( ) ;
620+ f. write_all ( pkg_json_content. as_bytes ( ) ) . unwrap ( ) ;
621+
622+ let scripts_dir = AbsolutePathBuf :: new ( dist_dir) . unwrap ( ) ;
623+ let executor = JsExecutor :: new ( Some ( scripts_dir) ) ;
624+
625+ let req = executor. get_cli_engines_requirement ( ) . await ;
626+ assert_eq ! ( req. as_deref( ) , Some ( "^20.19.0 || >=22.12.0" ) ) ;
627+ }
628+
629+ #[ tokio:: test]
630+ async fn get_cli_engines_requirement_returns_none_when_missing ( ) {
631+ use tempfile:: TempDir ;
632+
633+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
634+ let dist_dir = temp_dir. path ( ) . join ( "dist" ) ;
635+ std:: fs:: create_dir ( & dist_dir) . unwrap ( ) ;
636+
637+ // No package.json → should return None
638+ let scripts_dir = AbsolutePathBuf :: new ( dist_dir) . unwrap ( ) ;
639+ let executor = JsExecutor :: new ( Some ( scripts_dir) ) ;
640+
641+ let req = executor. get_cli_engines_requirement ( ) . await ;
642+ assert ! ( req. is_none( ) ) ;
643+ }
644+
529645 #[ tokio:: test]
530646 #[ serial]
531647 async fn test_delegate_to_local_cli_prints_node_version ( ) {
0 commit comments