11import { pathToFileURL } from 'url' ;
22import { default as IORedis } from 'ioredis' ;
33import { after } from 'lodash' ;
4+ import { EventEmitter } from 'events' ;
45import {
56 describe ,
67 beforeEach ,
@@ -20,8 +21,15 @@ import {
2021 UNRECOVERABLE_ERROR ,
2122 Worker ,
2223} from '../src/classes' ;
24+ import sandbox from '../src/classes/sandbox' ;
25+ import { ParentCommand } from '../src/enums' ;
2326
24- import { delay , randomUUID , removeAllQueueData } from '../src/utils' ;
27+ import {
28+ delay ,
29+ errorToJSON ,
30+ randomUUID ,
31+ removeAllQueueData ,
32+ } from '../src/utils' ;
2533import { existsSync , unlinkSync , writeFileSync } from 'fs' ;
2634const { stdout, stderr } = require ( 'test-console' ) ;
2735
@@ -229,6 +237,45 @@ describe('Sandboxed process using worker threads', () => {
229237 } ) ;
230238} ) ;
231239
240+ describe ( 'Sandbox error message handling' , ( ) => {
241+ // A child that refuses a Start (e.g. 'cannot start a not idling child
242+ // process') reports the reason via ParentCommand.Error, whose payload is
243+ // carried under the `err` key — unlike ParentCommand.Failed which uses
244+ // `value`. The sandbox message handler must read from either key so the
245+ // reason is never lost as an empty-message error. This guards the
246+ // `msg.value ?? msg.err` fix independently of the child lifecycle, since the
247+ // refusal path is otherwise hard to reach once init-failed children exit.
248+ it ( 'propagates the reason when ParentCommand.Error uses the `err` key' , async ( ) => {
249+ const reason = 'cannot start a not idling child process' ;
250+
251+ const fakeChild : any = new EventEmitter ( ) ;
252+ fakeChild . exitCode = null ;
253+ fakeChild . signalCode = null ;
254+ fakeChild . processFile = 'fake-process-file' ;
255+ fakeChild . pid = 1 ;
256+ fakeChild . send = ( ) => {
257+ // Simulate the child refusing the Start command and reporting the reason
258+ // under `err` (ParentCommand.Error), not `value` (ParentCommand.Failed).
259+ queueMicrotask ( ( ) => {
260+ fakeChild . emit ( 'message' , {
261+ cmd : ParentCommand . Error ,
262+ err : errorToJSON ( new Error ( reason ) ) ,
263+ } ) ;
264+ } ) ;
265+ } ;
266+
267+ const fakeChildPool : any = {
268+ retain : async ( ) => fakeChild ,
269+ release : ( ) => { } ,
270+ } ;
271+
272+ const processFn = sandbox ( 'fake-process-file' , fakeChildPool ) ;
273+ const fakeJob : any = { asJSONSandbox : ( ) => ( { } ) } ;
274+
275+ await expect ( processFn ( fakeJob ) ) . rejects . toThrow ( reason ) ;
276+ } ) ;
277+ } ) ;
278+
232279function sandboxProcessTests (
233280 { useWorkerThreads } = { useWorkerThreads : false } ,
234281) {
0 commit comments