55import java .io .IOException ;
66import java .io .InputStreamReader ;
77import java .util .ArrayList ;
8+ import java .util .LinkedHashSet ;
89import java .util .List ;
10+ import java .util .Locale ;
911import java .util .Map ;
12+ import java .util .Set ;
1013import java .util .concurrent .TimeUnit ;
1114
1215import com .google .gson .Gson ;
@@ -19,10 +22,10 @@ class ProtocolHostProcess implements AutoCloseable {
1922
2023 ProtocolHostProcess (int port , boolean autoPong , boolean sendControl , boolean dropPong ) throws IOException , InterruptedException {
2124 File script = new File ("src/test/resources/ProtocolHost.js" );
22- String node = "/usr/local/bin/node" ;
23- ProcessBuilder pb = new ProcessBuilder (node , script .getAbsolutePath ());
25+ ProcessBuilder pb = new ProcessBuilder ();
2426 Map <String , String > env = pb .environment ();
25- env .put ("PATH" , System .getenv ("PATH" ));
27+ String node = resolveNodeCommand (env );
28+ pb .command (node , script .getAbsolutePath ());
2629 env .put ("PORT" , Integer .toString (port ));
2730 env .put ("SECRET" , "dev-secret" );
2831 env .put ("AUTO_PONG" , autoPong ? "true" : "false" );
@@ -50,6 +53,79 @@ class ProtocolHostProcess implements AutoCloseable {
5053 }
5154 }
5255
56+ private static String resolveNodeCommand (Map <String , String > env ) throws IOException {
57+ // CODE_BRIDGE_NODE may point at a specific node binary for environments
58+ // where the Java test process does not inherit the expected PATH.
59+ String override = trimToNull (env .get ("CODE_BRIDGE_NODE" ));
60+ if (override != null ) {
61+ if (isPathLike (override )) {
62+ File file = new File (override );
63+ if (file .isFile () && file .canExecute ()) {
64+ return file .getAbsolutePath ();
65+ }
66+ throw new IOException ("CODE_BRIDGE_NODE must point to an executable node binary: " + override );
67+ }
68+
69+ String resolved = findExecutableOnPath (override , env .get ("PATH" ));
70+ if (resolved != null ) {
71+ return resolved ;
72+ }
73+ throw new IOException ("CODE_BRIDGE_NODE was set but not found on PATH: " + override );
74+ }
75+
76+ String resolved = findExecutableOnPath ("node" , env .get ("PATH" ));
77+ if (resolved != null ) {
78+ return resolved ;
79+ }
80+ throw new IOException ("node executable not found on PATH; set CODE_BRIDGE_NODE to the node binary for Java protocol tests" );
81+ }
82+
83+ private static String findExecutableOnPath (String command , String path ) {
84+ if (trimToNull (path ) == null ) {
85+ return null ;
86+ }
87+
88+ for (String dir : path .split (File .pathSeparator )) {
89+ if (dir .isBlank ()) {
90+ continue ;
91+ }
92+ for (String candidate : executableCandidates (command )) {
93+ File file = new File (dir , candidate );
94+ if (file .isFile () && file .canExecute ()) {
95+ return file .getAbsolutePath ();
96+ }
97+ }
98+ }
99+ return null ;
100+ }
101+
102+ private static Set <String > executableCandidates (String command ) {
103+ Set <String > candidates = new LinkedHashSet <>();
104+ candidates .add (command );
105+
106+ String pathExt = System .getenv ("PATHEXT" );
107+ if (pathExt != null ) {
108+ for (String extension : pathExt .split (File .pathSeparator )) {
109+ if (!extension .isBlank ()
110+ && !command .toLowerCase (Locale .ROOT ).endsWith (extension .toLowerCase (Locale .ROOT ))) {
111+ candidates .add (command + extension );
112+ }
113+ }
114+ }
115+ return candidates ;
116+ }
117+
118+ private static boolean isPathLike (String command ) {
119+ return new File (command ).isAbsolute () || command .contains ("/" ) || command .contains ("\\ " );
120+ }
121+
122+ private static String trimToNull (String value ) {
123+ if (value == null || value .trim ().isEmpty ()) {
124+ return null ;
125+ }
126+ return value .trim ();
127+ }
128+
53129 List <Map <String , Object >> readEvents (long timeoutMs ) throws IOException {
54130 long deadline = System .currentTimeMillis () + timeoutMs ;
55131 List <Map <String , Object >> events = new ArrayList <>();
0 commit comments