1- import { type ChildProcess , exec } from 'node:child_process' ;
1+ import { type ChildProcess , spawn } from 'node:child_process' ;
22import { existsSync , unlinkSync , writeFileSync } from 'node:fs' ;
3+ import { connect } from 'node:net' ;
34import { resolve } from 'node:path' ;
45
56let serverProcess : ChildProcess | null = null ;
@@ -8,6 +9,21 @@ let serverStartPromise: Promise<void> | null = null;
89// File-based lock to coordinate between parallel workers
910const LOCK_FILE = resolve ( __dirname , '../../.server-starting.lock' ) ;
1011
12+ export async function stopWebServer ( ) : Promise < void > {
13+ if ( serverProcess ) {
14+ console . log ( '🛑 Stopping web server...' ) ;
15+ serverProcess . kill ( ) ;
16+ serverProcess = null ;
17+ serverStartPromise = null ;
18+ }
19+ // Clean up lock file
20+ try {
21+ unlinkSync ( LOCK_FILE ) ;
22+ } catch {
23+ // Ignore if file doesn't exist
24+ }
25+ }
26+
1127interface WebServerOptions {
1228 command : string ;
1329 env ?: Record < string , string > ;
@@ -16,15 +32,36 @@ interface WebServerOptions {
1632 timeout ?: number ;
1733}
1834
19- async function isServerRunning ( port : number ) : Promise < boolean > {
20- try {
21- const response = await fetch ( `http://localhost:${ port } /chat` , {
22- method : 'HEAD' ,
35+ async function isServerRunning ( port : number , timeoutMs = 2000 ) : Promise < boolean > {
36+ const hosts = new Set ( [ '127.0.0.1' , '::1' , 'localhost' ] ) ;
37+ const envHost = process . env . HOST ?. trim ( ) ;
38+ if ( envHost && envHost !== '0.0.0.0' && envHost !== '::' ) {
39+ hosts . add ( envHost ) ;
40+ }
41+
42+ const tryConnect = ( host : string ) =>
43+ new Promise < boolean > ( ( resolve ) => {
44+ const socket = connect ( { host, port } ) ;
45+ const timeoutId = setTimeout ( ( ) => {
46+ socket . destroy ( ) ;
47+ resolve ( false ) ;
48+ } , timeoutMs ) ;
49+
50+ const finish = ( result : boolean ) => {
51+ clearTimeout ( timeoutId ) ;
52+ socket . destroy ( ) ;
53+ resolve ( result ) ;
54+ } ;
55+
56+ socket . once ( 'connect' , ( ) => finish ( true ) ) ;
57+ socket . once ( 'error' , ( ) => finish ( false ) ) ;
2358 } ) ;
24- return response . ok ;
25- } catch {
26- return false ;
59+
60+ for ( const host of hosts ) {
61+ if ( await tryConnect ( host ) ) return true ;
2762 }
63+
64+ return false ;
2865}
2966
3067export async function startWebServer ( options : WebServerOptions ) : Promise < void > {
@@ -94,27 +131,41 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
94131 // Get the project root directory (parent of e2e folder)
95132 const projectRoot = resolve ( __dirname , '../../..' ) ;
96133
97- // Start the server process
98- serverProcess = exec ( command , {
134+ const serverEnv = {
135+ ...process . env ,
136+ // E2E test secret keys
137+ BETTER_AUTH_SECRET : 'e2e-test-secret-key-for-better-auth-32chars!' ,
138+ KEY_VAULTS_SECRET : 'LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=' ,
139+ // Disable email verification for e2e
140+ NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION : '0' ,
141+ // Enable Better Auth for e2e tests with real authentication
142+ NEXT_PUBLIC_ENABLE_BETTER_AUTH : '1' ,
143+ NODE_OPTIONS : '--max-old-space-size=6144' ,
144+ PORT : String ( port ) ,
145+ // Mock S3 env vars to prevent initialization errors
146+ S3_ACCESS_KEY_ID : 'e2e-mock-access-key' ,
147+ S3_BUCKET : 'e2e-mock-bucket' ,
148+ S3_ENDPOINT : 'https://e2e-mock-s3.localhost' ,
149+ S3_SECRET_ACCESS_KEY : 'e2e-mock-secret-key' ,
150+ ...env ,
151+ } ;
152+
153+ // Start the server process (spawn avoids maxBuffer limits)
154+ serverProcess = spawn ( command , {
99155 cwd : projectRoot ,
100- env : {
101- ...process . env ,
102- // E2E test secret keys
103- BETTER_AUTH_SECRET : 'e2e-test-secret-key-for-better-auth-32chars!' ,
104- KEY_VAULTS_SECRET : 'LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=' ,
105- // Disable email verification for e2e
106- NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION : '0' ,
107- // Enable Better Auth for e2e tests with real authentication
108- NEXT_PUBLIC_ENABLE_BETTER_AUTH : '1' ,
109- NODE_OPTIONS : '--max-old-space-size=6144' ,
110- PORT : String ( port ) ,
111- // Mock S3 env vars to prevent initialization errors
112- S3_ACCESS_KEY_ID : 'e2e-mock-access-key' ,
113- S3_BUCKET : 'e2e-mock-bucket' ,
114- S3_ENDPOINT : 'https://e2e-mock-s3.localhost' ,
115- S3_SECRET_ACCESS_KEY : 'e2e-mock-secret-key' ,
116- ...env ,
117- } ,
156+ env : serverEnv ,
157+ shell : true ,
158+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
159+ } ) ;
160+
161+ let startupError : Error | null = null ;
162+ serverProcess . once ( 'error' , ( error ) => {
163+ startupError = error ;
164+ } ) ;
165+ serverProcess . once ( 'exit' , ( code , signal ) => {
166+ startupError = new Error (
167+ `Server exited before ready (code: ${ code ?? 'unknown' } , signal: ${ signal ?? 'none' } )` ,
168+ ) ;
118169 } ) ;
119170
120171 // Forward server output to console for debugging
@@ -129,6 +180,9 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
129180 // Wait for server to be ready
130181 const startTime = Date . now ( ) ;
131182 while ( ! ( await isServerRunning ( port ) ) ) {
183+ if ( startupError ) {
184+ throw startupError ;
185+ }
132186 if ( Date . now ( ) - startTime > timeout ) {
133187 throw new Error ( `Server failed to start within ${ timeout } ms` ) ;
134188 }
@@ -138,22 +192,20 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
138192 }
139193
140194 console . log ( `✅ Web server is ready on port ${ port } ` ) ;
141- } ) ( ) ;
195+ } ) ( ) . catch ( async ( error ) => {
196+ serverStartPromise = null ;
197+ try {
198+ unlinkSync ( LOCK_FILE ) ;
199+ await stopWebServer ( ) ;
200+ } catch {
201+ // Ignore if file doesn't exist
202+ }
203+ throw error ;
204+ } ) ;
142205
143206 return serverStartPromise ;
144207}
145208
146- export async function stopWebServer ( ) : Promise < void > {
147- if ( serverProcess ) {
148- console . log ( '🛑 Stopping web server...' ) ;
149- serverProcess . kill ( ) ;
150- serverProcess = null ;
151- serverStartPromise = null ;
152- }
153- // Clean up lock file
154- try {
155- unlinkSync ( LOCK_FILE ) ;
156- } catch {
157- // Ignore if file doesn't exist
158- }
159- }
209+ process . on ( 'exit' , ( ) => {
210+ void stopWebServer ( ) ;
211+ } ) ;
0 commit comments