@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
55import { Worker } from 'worker_threads'
66import { EventEmitter } from 'events'
77import ms from 'ms'
8+ import merge from 'lodash.merge'
89
910const __filename = fileURLToPath ( import . meta. url )
1011const __dirname = dirname ( __filename )
@@ -66,21 +67,21 @@ const createWorker = (workerObject, isPoolMode = false) => {
6667 stdout : true ,
6768 stderr : true ,
6869 } )
69-
70+
7071 // Pipe worker stdout/stderr to main process
7172 if ( worker . stdout ) {
7273 worker . stdout . setEncoding ( 'utf8' )
73- worker . stdout . on ( 'data' , ( data ) => {
74+ worker . stdout . on ( 'data' , data => {
7475 process . stdout . write ( data )
7576 } )
7677 }
7778 if ( worker . stderr ) {
7879 worker . stderr . setEncoding ( 'utf8' )
79- worker . stderr . on ( 'data' , ( data ) => {
80+ worker . stderr . on ( 'data' , data => {
8081 process . stderr . write ( data )
8182 } )
8283 }
83-
84+
8485 worker . on ( 'error' , err => {
8586 console . error ( `[Main] Worker Error:` , err )
8687 output . error ( `Worker Error: ${ err . stack } ` )
@@ -221,13 +222,13 @@ class WorkerObject {
221222
222223 addConfig ( config ) {
223224 const oldConfig = JSON . parse ( this . options . override || '{}' )
224-
225+
225226 // Remove customLocatorStrategies from both old and new config before JSON serialization
226227 // since functions cannot be serialized and will be lost, causing workers to have empty strategies
227228 const configWithoutFunctions = { ...config }
228-
229+
229230 // Clean both old and new config
230- const cleanConfig = ( cfg ) => {
231+ const cleanConfig = cfg => {
231232 if ( cfg . helpers ) {
232233 cfg . helpers = { ...cfg . helpers }
233234 Object . keys ( cfg . helpers ) . forEach ( helperName => {
@@ -239,14 +240,12 @@ class WorkerObject {
239240 }
240241 return cfg
241242 }
242-
243+
243244 const cleanedOldConfig = cleanConfig ( oldConfig )
244245 const cleanedNewConfig = cleanConfig ( configWithoutFunctions )
245-
246- const newConfig = {
247- ...cleanedOldConfig ,
248- ...cleanedNewConfig ,
249- }
246+
247+ // Deep merge configurations to preserve all helpers from base config
248+ const newConfig = merge ( { } , cleanedOldConfig , cleanedNewConfig )
250249 this . options . override = JSON . stringify ( newConfig )
251250 }
252251
@@ -280,8 +279,8 @@ class Workers extends EventEmitter {
280279 this . setMaxListeners ( 50 )
281280 this . codeceptPromise = initializeCodecept ( config . testConfig , config . options )
282281 this . codecept = null
283- this . config = config // Save config
284- this . numberOfWorkersRequested = numberOfWorkers // Save requested worker count
282+ this . config = config // Save config
283+ this . numberOfWorkersRequested = numberOfWorkers // Save requested worker count
285284 this . options = config . options || { }
286285 this . errors = [ ]
287286 this . numberOfWorkers = 0
@@ -304,11 +303,8 @@ class Workers extends EventEmitter {
304303 // Initialize workers in these cases:
305304 // 1. Positive number requested AND no manual workers pre-spawned
306305 // 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned
307- const shouldAutoInit = this . workers . length === 0 && (
308- ( Number . isInteger ( this . numberOfWorkersRequested ) && this . numberOfWorkersRequested > 0 ) ||
309- ( this . numberOfWorkersRequested < 0 && isFunction ( this . config . by ) )
310- )
311-
306+ const shouldAutoInit = this . workers . length === 0 && ( ( Number . isInteger ( this . numberOfWorkersRequested ) && this . numberOfWorkersRequested > 0 ) || ( this . numberOfWorkersRequested < 0 && isFunction ( this . config . by ) ) )
307+
312308 if ( shouldAutoInit ) {
313309 this . _initWorkers ( this . numberOfWorkersRequested , this . config )
314310 }
@@ -371,9 +367,9 @@ class Workers extends EventEmitter {
371367 * @param {Number } numberOfWorkers
372368 */
373369 createGroupsOfTests ( numberOfWorkers ) {
374- // If Codecept isn't initialized yet, return empty groups as a safe fallback
375- if ( ! this . codecept ) return populateGroups ( numberOfWorkers )
376- const files = this . codecept . testFiles
370+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
371+ if ( ! this . codecept ) return populateGroups ( numberOfWorkers )
372+ const files = this . codecept . testFiles
377373 const mocha = Container . mocha ( )
378374 mocha . files = files
379375 mocha . loadFiles ( )
@@ -430,7 +426,7 @@ class Workers extends EventEmitter {
430426 for ( const file of files ) {
431427 this . testPool . push ( file )
432428 }
433-
429+
434430 this . testPoolInitialized = true
435431 }
436432
@@ -443,17 +439,17 @@ class Workers extends EventEmitter {
443439 if ( ! this . testPoolInitialized ) {
444440 this . _initializeTestPool ( )
445441 }
446-
442+
447443 return this . testPool . shift ( )
448444 }
449445
450446 /**
451447 * @param {Number } numberOfWorkers
452448 */
453449 createGroupsOfSuites ( numberOfWorkers ) {
454- // If Codecept isn't initialized yet, return empty groups as a safe fallback
455- if ( ! this . codecept ) return populateGroups ( numberOfWorkers )
456- const files = this . codecept . testFiles
450+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
451+ if ( ! this . codecept ) return populateGroups ( numberOfWorkers )
452+ const files = this . codecept . testFiles
457453 const groups = populateGroups ( numberOfWorkers )
458454
459455 const mocha = Container . mocha ( )
@@ -494,7 +490,7 @@ class Workers extends EventEmitter {
494490 recorder . startUnlessRunning ( )
495491 event . dispatcher . emit ( event . workers . before )
496492 process . env . RUNS_WITH_WORKERS = 'true'
497-
493+
498494 // Create workers and set up message handlers immediately (not in recorder queue)
499495 // This prevents a race condition where workers start sending messages before handlers are attached
500496 const workerThreads = [ ]
@@ -503,11 +499,11 @@ class Workers extends EventEmitter {
503499 this . _listenWorkerEvents ( workerThread )
504500 workerThreads . push ( workerThread )
505501 }
506-
502+
507503 recorder . add ( 'workers started' , ( ) => {
508504 // Workers are already running, this is just a placeholder step
509505 } )
510-
506+
511507 return new Promise ( resolve => {
512508 this . on ( 'end' , resolve )
513509 } )
@@ -591,7 +587,7 @@ class Workers extends EventEmitter {
591587 // Otherwise skip - we'll emit based on finished state
592588 break
593589 case event . test . passed :
594- // Skip individual passed events - we'll emit based on finished state
590+ // Skip individual passed events - we'll emit based on finished state
595591 break
596592 case event . test . skipped :
597593 this . emit ( event . test . skipped , deserializeTest ( message . data ) )
@@ -602,15 +598,15 @@ class Workers extends EventEmitter {
602598 const data = message . data
603599 const uid = data ?. uid
604600 const isFailed = ! ! data ?. err || data ?. state === 'failed'
605-
601+
606602 if ( uid ) {
607603 // Track states for each test UID
608604 if ( ! this . _testStates ) this . _testStates = new Map ( )
609-
605+
610606 if ( ! this . _testStates . has ( uid ) ) {
611607 this . _testStates . set ( uid , { states : [ ] , lastData : data } )
612608 }
613-
609+
614610 const testState = this . _testStates . get ( uid )
615611 testState . states . push ( { isFailed, data } )
616612 testState . lastData = data
@@ -622,7 +618,7 @@ class Workers extends EventEmitter {
622618 this . emit ( event . test . passed , deserializeTest ( data ) )
623619 }
624620 }
625-
621+
626622 this . emit ( event . test . finished , deserializeTest ( data ) )
627623 }
628624 break
@@ -682,11 +678,10 @@ class Workers extends EventEmitter {
682678 // For tests with retries configured, emit all failures + final success
683679 // For tests without retries, emit only final state
684680 const lastState = states [ states . length - 1 ]
685-
681+
686682 // Check if this test had retries by looking for failure followed by success
687- const hasRetryPattern = states . length > 1 &&
688- states . some ( ( s , i ) => s . isFailed && i < states . length - 1 && ! states [ i + 1 ] . isFailed )
689-
683+ const hasRetryPattern = states . length > 1 && states . some ( ( s , i ) => s . isFailed && i < states . length - 1 && ! states [ i + 1 ] . isFailed )
684+
690685 if ( hasRetryPattern ) {
691686 // Emit all intermediate failures and final success for retries
692687 for ( const state of states ) {
0 commit comments