@@ -901,4 +901,190 @@ mod tests {
901901 assert ! ( all_envs. contains_key( "app1_name" ) ) ;
902902 assert ! ( all_envs. contains_key( "app2_name" ) ) ;
903903 }
904+
905+ #[ test]
906+ #[ cfg( windows) ]
907+ fn test_windows_path_case_insensitive_mixed_case ( ) {
908+ use crate :: {
909+ collections:: HashSet ,
910+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
911+ } ;
912+
913+ let task_config = TaskConfig {
914+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
915+ cwd : RelativePathBuf :: default ( ) ,
916+ cacheable : true ,
917+ inputs : HashSet :: new ( ) ,
918+ envs : HashSet :: new ( ) ,
919+ pass_through_envs : HashSet :: new ( ) ,
920+ fingerprint_ignores : None ,
921+ } ;
922+ let resolved =
923+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
924+
925+ // Mock environment with mixed case "Path" (common on Windows)
926+ let mock_envs = vec ! [
927+ ( OsString :: from( "Path" ) , OsString :: from( "C:\\ existing\\ path" ) ) ,
928+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
929+ ] ;
930+
931+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
932+
933+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
934+
935+ let all_envs = result. all_envs ;
936+
937+ // Verify that the original "Path" casing is preserved, not "PATH"
938+ assert ! ( all_envs. contains_key( "Path" ) ) ;
939+ assert ! ( !all_envs. contains_key( "PATH" ) ) ;
940+
941+ // Verify the complete PATH value matches expected
942+ let path_value = all_envs. get ( "Path" ) . unwrap ( ) ;
943+ assert_eq ! (
944+ path_value. as_ref( ) ,
945+ OsStr :: new(
946+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ existing\\ path"
947+ )
948+ ) ;
949+
950+ // Verify no duplicate PATH entry was created
951+ let path_like_keys: Vec < _ > =
952+ all_envs. keys ( ) . filter ( |k| k. eq_ignore_ascii_case ( "path" ) ) . collect ( ) ;
953+ assert_eq ! ( path_like_keys. len( ) , 1 ) ;
954+ }
955+
956+ #[ test]
957+ #[ cfg( windows) ]
958+ fn test_windows_path_case_insensitive_uppercase ( ) {
959+ use crate :: {
960+ collections:: HashSet ,
961+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
962+ } ;
963+
964+ let task_config = TaskConfig {
965+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
966+ cwd : RelativePathBuf :: default ( ) ,
967+ cacheable : true ,
968+ inputs : HashSet :: new ( ) ,
969+ envs : HashSet :: new ( ) ,
970+ pass_through_envs : HashSet :: new ( ) ,
971+ fingerprint_ignores : None ,
972+ } ;
973+ let resolved =
974+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
975+
976+ // Mock environment with uppercase "PATH"
977+ let mock_envs = vec ! [
978+ ( OsString :: from( "PATH" ) , OsString :: from( "C:\\ existing\\ path" ) ) ,
979+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
980+ ] ;
981+
982+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
983+
984+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
985+
986+ let all_envs = result. all_envs ;
987+
988+ // Verify that "PATH" is preserved
989+ assert ! ( all_envs. contains_key( "PATH" ) ) ;
990+
991+ // Verify the complete PATH value matches expected
992+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
993+ assert_eq ! (
994+ path_value. as_ref( ) ,
995+ OsStr :: new(
996+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ existing\\ path"
997+ )
998+ ) ;
999+ }
1000+
1001+ #[ test]
1002+ #[ cfg( windows) ]
1003+ fn test_windows_path_created_when_missing ( ) {
1004+ use crate :: {
1005+ collections:: HashSet ,
1006+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
1007+ } ;
1008+
1009+ let task_config = TaskConfig {
1010+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
1011+ cwd : RelativePathBuf :: default ( ) ,
1012+ cacheable : true ,
1013+ inputs : HashSet :: new ( ) ,
1014+ envs : HashSet :: new ( ) ,
1015+ pass_through_envs : HashSet :: new ( ) ,
1016+ fingerprint_ignores : None ,
1017+ } ;
1018+ let resolved =
1019+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
1020+
1021+ // Mock environment without any PATH variable
1022+ let mock_envs = vec ! [ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ] ;
1023+
1024+ let base_dir = AbsolutePath :: new ( "C:\\ workspace\\ packages\\ app" ) . unwrap ( ) ;
1025+
1026+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
1027+
1028+ let all_envs = result. all_envs ;
1029+
1030+ // Verify that "PATH" is created when missing
1031+ assert ! ( all_envs. contains_key( "PATH" ) ) ;
1032+
1033+ // Verify the complete PATH value matches expected (only node_modules/.bin paths, no existing path)
1034+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
1035+ assert_eq ! (
1036+ path_value. as_ref( ) ,
1037+ OsStr :: new(
1038+ "C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin;C:\\ workspace\\ packages\\ app\\ node_modules\\ .bin"
1039+ )
1040+ ) ;
1041+ }
1042+
1043+ #[ test]
1044+ #[ cfg( unix) ]
1045+ fn test_unix_path_case_sensitive ( ) {
1046+ use crate :: {
1047+ collections:: HashSet ,
1048+ config:: { ResolvedTaskConfig , TaskCommand , TaskConfig } ,
1049+ } ;
1050+
1051+ let task_config = TaskConfig {
1052+ command : TaskCommand :: ShellScript ( "echo test" . into ( ) ) ,
1053+ cwd : RelativePathBuf :: default ( ) ,
1054+ cacheable : true ,
1055+ inputs : HashSet :: new ( ) ,
1056+ envs : HashSet :: new ( ) ,
1057+ pass_through_envs : HashSet :: new ( ) ,
1058+ fingerprint_ignores : None ,
1059+ } ;
1060+ let resolved =
1061+ ResolvedTaskConfig { config_dir : RelativePathBuf :: default ( ) , config : task_config } ;
1062+
1063+ // Mock environment with "PATH" in uppercase (standard on Unix)
1064+ let mock_envs = vec ! [
1065+ ( OsString :: from( "PATH" ) , OsString :: from( "/existing/path" ) ) ,
1066+ ( OsString :: from( "OTHER_VAR" ) , OsString :: from( "value" ) ) ,
1067+ ] ;
1068+
1069+ let base_dir = AbsolutePath :: new ( "/workspace/packages/app" ) . unwrap ( ) ;
1070+
1071+ let result = TaskEnvs :: resolve ( mock_envs. into_iter ( ) , & base_dir, & resolved) . unwrap ( ) ;
1072+
1073+ let all_envs = result. all_envs ;
1074+
1075+ // Verify "PATH" exists and the complete value matches expected
1076+ assert ! ( all_envs. contains_key( "PATH" ) ) ;
1077+ let path_value = all_envs. get ( "PATH" ) . unwrap ( ) ;
1078+ assert_eq ! (
1079+ path_value. as_ref( ) ,
1080+ OsStr :: new(
1081+ "/workspace/packages/app/node_modules/.bin:/workspace/packages/app/node_modules/.bin:/existing/path"
1082+ )
1083+ ) ;
1084+
1085+ // Verify that on Unix, the code uses exact "PATH" match (case-sensitive)
1086+ // This is a regression test to ensure Windows case-insensitive logic doesn't affect Unix
1087+ assert ! ( !all_envs. contains_key( "Path" ) ) ;
1088+ assert ! ( !all_envs. contains_key( "path" ) ) ;
1089+ }
9041090}
0 commit comments