@@ -17,141 +17,149 @@ import {
1717 * parseError:shutdown).
1818 */
1919
20- describe ( 'WorkerPool end-to-end with a local fixture plugin' , ( ) => {
21- test ( 'cancelTask inside onTaskDispatched aborts before worker runs' , async ( ) => {
22- const pool = new WorkerPool ( {
23- configs : localConfigs ,
24- workerCount : 1 ,
25- taskTimeoutMs : 10_000 ,
26- } ) ;
27- await pool . init ( ) ;
28-
29- const tasks : LintTask [ ] = [ task ( 'a.ts' , 'const x = null;' ) ] ;
30-
31- const dispatchedIds : number [ ] = [ ] ;
32- const results = await pool . lintBatch ( tasks , ( taskId ) => {
33- dispatchedIds . push ( taskId ) ;
34- expect ( pool . cancelTask ( taskId ) ) . toBe ( true ) ;
35- } ) ;
36-
37- expect ( dispatchedIds ) . toHaveLength ( 1 ) ;
38- expect ( results ) . toHaveLength ( 1 ) ;
39- expect ( results [ 0 ] . cancelled ) . toBe ( true ) ;
40- expect ( results [ 0 ] . diagnostics ) . toHaveLength ( 0 ) ;
41-
42- await pool . shutdown ( ) ;
43- } , 20_000 ) ;
44-
45- test ( 'queue: cancelTask drops a queued (not-yet-dispatched) task without posting to worker' , async ( ) => {
46- // Drive the queue path: with the worker forced non-idle, the
47- // task lands in pendingQueue. cancelTask on its id must mark
48- // it cancelled. Releasing the worker (kickQueue) then resolves
49- // it as cancelled=true without ever posting to the worker.
50- const pool = new WorkerPool ( {
51- configs : localConfigs ,
52- workerCount : 1 ,
53- } ) ;
54- await pool . init ( ) ;
55- try {
20+ // Skipped on windows: tearing down a worker that has oxc (a napi addon)
21+ // loaded aborts below the JS layer there (nodejs/node#34567) and crashes
22+ // the rstest worker running this file. These e2e tests spawn real
23+ // workers and tear them down, so they are windows-skipped; they still
24+ // run on linux/macOS.
25+ describe . skipIf ( process . platform === 'win32' ) (
26+ 'WorkerPool end-to-end with a local fixture plugin' ,
27+ ( ) => {
28+ test ( 'cancelTask inside onTaskDispatched aborts before worker runs' , async ( ) => {
29+ const pool = new WorkerPool ( {
30+ configs : localConfigs ,
31+ workerCount : 1 ,
32+ taskTimeoutMs : 10_000 ,
33+ } ) ;
34+ await pool . init ( ) ;
35+
36+ const tasks : LintTask [ ] = [ task ( 'a.ts' , 'const x = null;' ) ] ;
37+
38+ const dispatchedIds : number [ ] = [ ] ;
39+ const results = await pool . lintBatch ( tasks , ( taskId ) => {
40+ dispatchedIds . push ( taskId ) ;
41+ expect ( pool . cancelTask ( taskId ) ) . toBe ( true ) ;
42+ } ) ;
43+
44+ expect ( dispatchedIds ) . toHaveLength ( 1 ) ;
45+ expect ( results ) . toHaveLength ( 1 ) ;
46+ expect ( results [ 0 ] . cancelled ) . toBe ( true ) ;
47+ expect ( results [ 0 ] . diagnostics ) . toHaveLength ( 0 ) ;
48+
49+ await pool . shutdown ( ) ;
50+ } , 20_000 ) ;
51+
52+ test ( 'queue: cancelTask drops a queued (not-yet-dispatched) task without posting to worker' , async ( ) => {
53+ // Drive the queue path: with the worker forced non-idle, the
54+ // task lands in pendingQueue. cancelTask on its id must mark
55+ // it cancelled. Releasing the worker (kickQueue) then resolves
56+ // it as cancelled=true without ever posting to the worker.
57+ const pool = new WorkerPool ( {
58+ configs : localConfigs ,
59+ workerCount : 1 ,
60+ } ) ;
61+ await pool . init ( ) ;
62+ try {
63+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64+ const ws = ( pool as any ) . workers as Array < { ready : boolean } > ;
65+ ws [ 0 ] . ready = false ;
66+
67+ let capturedId = - 1 ;
68+ const batchP = pool . lintBatch (
69+ [
70+ {
71+ filePath : 'q.ts' ,
72+ text : 'const x = null;\n' ,
73+ rules : { 'local/no-null' : { options : [ ] } } ,
74+ collectFixes : false ,
75+ suggestionsMode : 'off' ,
76+ configKey : LOCAL_CONFIG_DIR ,
77+ } ,
78+ ] ,
79+ ( taskId ) => {
80+ capturedId = taskId ;
81+ } ,
82+ ) ;
83+ expect ( capturedId ) . toBeGreaterThan ( 0 ) ;
84+ const cancelled = pool . cancelTask ( capturedId ) ;
85+ expect ( cancelled ) . toBe ( true ) ;
86+
87+ // Release the worker — kickQueue should see the cancelled
88+ // marker and resolve without postMessage.
89+ ws [ 0 ] . ready = true ;
90+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91+ ( pool as any ) . kickQueue ( ) ;
92+
93+ const results = await batchP ;
94+ expect ( results ) . toHaveLength ( 1 ) ;
95+ expect ( results [ 0 ] . cancelled ) . toBe ( true ) ;
96+ expect ( results [ 0 ] . diagnostics ) . toEqual ( [ ] ) ;
97+ // Task was never dispatched, so no parseError sentinel either.
98+ expect ( results [ 0 ] . parseError ) . toBeUndefined ( ) ;
99+ } finally {
100+ await pool . shutdown ( ) ;
101+ }
102+ } , 15_000 ) ;
103+
104+ test ( 'shutdown drain honors q.cancelled — cancelled-then-shutdown reports cancelled:true, not parseError:shutdown' , async ( ) => {
105+ // Regression for review P2 #15. Pre-fix, the drain loop in
106+ // `shutdown` blindly stamped every queued task as
107+ // `cancelled: false, parseError: 'shutdown'` regardless of whether
108+ // the user had already called `cancelTask(taskId)` on it. The
109+ // `q.cancelled` flag (set by cancelTask for queued entries) was
110+ // ignored, so a user-initiated cancel that hadn't been picked up
111+ // by `kickQueue` yet would be mis-labelled as a shutdown failure
112+ // — a meaningful distinction for LSP result categorisation and
113+ // strict-runner error counters.
114+ const pool = new WorkerPool ( {
115+ configs : localConfigs ,
116+ workerCount : 1 ,
117+ } ) ;
118+ await pool . init ( ) ;
119+ // Same wedge as R1: hold the worker non-ready so kickQueue can't
120+ // dispatch, leaving every task in pendingQueue.
56121 // eslint-disable-next-line @typescript-eslint/no-explicit-any
57122 const ws = ( pool as any ) . workers as Array < { ready : boolean } > ;
58123 ws [ 0 ] . ready = false ;
59124
60- let capturedId = - 1 ;
125+ // Capture taskIds as they get assigned so we can cancel a
126+ // specific queued one.
127+ const taskIds : number [ ] = [ ] ;
61128 const batchP = pool . lintBatch (
62- [
63- {
64- filePath : 'q.ts' ,
65- text : 'const x = null;\n' ,
66- rules : { 'local/no-null' : { options : [ ] } } ,
67- collectFixes : false ,
68- suggestionsMode : 'off' ,
69- configKey : LOCAL_CONFIG_DIR ,
70- } ,
71- ] ,
129+ [ 1 , 2 , 3 ] . map ( ( i ) => ( {
130+ filePath : `q${ i } .ts` ,
131+ text : 'const x = null;\n' ,
132+ rules : { 'local/no-null' : { options : [ ] } } ,
133+ collectFixes : false ,
134+ suggestionsMode : 'off' ,
135+ configKey : LOCAL_CONFIG_DIR ,
136+ } ) ) ,
72137 ( taskId ) => {
73- capturedId = taskId ;
138+ taskIds . push ( taskId ) ;
74139 } ,
75140 ) ;
76- expect ( capturedId ) . toBeGreaterThan ( 0 ) ;
77- const cancelled = pool . cancelTask ( capturedId ) ;
78- expect ( cancelled ) . toBe ( true ) ;
79141
80- // Release the worker — kickQueue should see the cancelled
81- // marker and resolve without postMessage.
82- ws [ 0 ] . ready = true ;
83- // eslint-disable-next-line @typescript-eslint/no-explicit-any
84- ( pool as any ) . kickQueue ( ) ;
142+ // Let enqueue finish so onTaskDispatched fires for all 3.
143+ await new Promise ( ( r ) => setTimeout ( r , 30 ) ) ;
144+ expect ( taskIds ) . toHaveLength ( 3 ) ;
145+
146+ // Cancel the MIDDLE queued task — it must surface as
147+ // `cancelled: true` from the shutdown drain, while the
148+ // first/third stay as `parseError: 'shutdown'`.
149+ pool . cancelTask ( taskIds [ 1 ] ) ;
85150
86- const results = await batchP ;
87- expect ( results ) . toHaveLength ( 1 ) ;
88- expect ( results [ 0 ] . cancelled ) . toBe ( true ) ;
89- expect ( results [ 0 ] . diagnostics ) . toEqual ( [ ] ) ;
90- // Task was never dispatched, so no parseError sentinel either.
91- expect ( results [ 0 ] . parseError ) . toBeUndefined ( ) ;
92- } finally {
93151 await pool . shutdown ( ) ;
94- }
95- } , 15_000 ) ;
96-
97- test ( 'shutdown drain honors q.cancelled — cancelled-then-shutdown reports cancelled:true, not parseError:shutdown' , async ( ) => {
98- // Regression for review P2 #15. Pre-fix, the drain loop in
99- // `shutdown` blindly stamped every queued task as
100- // `cancelled: false, parseError: 'shutdown'` regardless of whether
101- // the user had already called `cancelTask(taskId)` on it. The
102- // `q.cancelled` flag (set by cancelTask for queued entries) was
103- // ignored, so a user-initiated cancel that hadn't been picked up
104- // by `kickQueue` yet would be mis-labelled as a shutdown failure
105- // — a meaningful distinction for LSP result categorisation and
106- // strict-runner error counters.
107- const pool = new WorkerPool ( {
108- configs : localConfigs ,
109- workerCount : 1 ,
110- } ) ;
111- await pool . init ( ) ;
112- // Same wedge as R1: hold the worker non-ready so kickQueue can't
113- // dispatch, leaving every task in pendingQueue.
114- // eslint-disable-next-line @typescript-eslint/no-explicit-any
115- const ws = ( pool as any ) . workers as Array < { ready : boolean } > ;
116- ws [ 0 ] . ready = false ;
117-
118- // Capture taskIds as they get assigned so we can cancel a
119- // specific queued one.
120- const taskIds : number [ ] = [ ] ;
121- const batchP = pool . lintBatch (
122- [ 1 , 2 , 3 ] . map ( ( i ) => ( {
123- filePath : `q${ i } .ts` ,
124- text : 'const x = null;\n' ,
125- rules : { 'local/no-null' : { options : [ ] } } ,
126- collectFixes : false ,
127- suggestionsMode : 'off' ,
128- configKey : LOCAL_CONFIG_DIR ,
129- } ) ) ,
130- ( taskId ) => {
131- taskIds . push ( taskId ) ;
132- } ,
133- ) ;
134-
135- // Let enqueue finish so onTaskDispatched fires for all 3.
136- await new Promise ( ( r ) => setTimeout ( r , 30 ) ) ;
137- expect ( taskIds ) . toHaveLength ( 3 ) ;
138-
139- // Cancel the MIDDLE queued task — it must surface as
140- // `cancelled: true` from the shutdown drain, while the
141- // first/third stay as `parseError: 'shutdown'`.
142- pool . cancelTask ( taskIds [ 1 ] ) ;
143-
144- await pool . shutdown ( ) ;
145- const result = await batchP ;
146- expect ( result ) . toHaveLength ( 3 ) ;
147-
148- expect ( result [ 0 ] . parseError ) . toBe ( 'shutdown' ) ;
149- expect ( result [ 0 ] . cancelled ) . toBe ( false ) ;
150-
151- expect ( result [ 1 ] . cancelled ) . toBe ( true ) ;
152- expect ( result [ 1 ] . parseError ) . toBeUndefined ( ) ;
153-
154- expect ( result [ 2 ] . parseError ) . toBe ( 'shutdown' ) ;
155- expect ( result [ 2 ] . cancelled ) . toBe ( false ) ;
156- } , 15_000 ) ;
157- } ) ;
152+ const result = await batchP ;
153+ expect ( result ) . toHaveLength ( 3 ) ;
154+
155+ expect ( result [ 0 ] . parseError ) . toBe ( 'shutdown' ) ;
156+ expect ( result [ 0 ] . cancelled ) . toBe ( false ) ;
157+
158+ expect ( result [ 1 ] . cancelled ) . toBe ( true ) ;
159+ expect ( result [ 1 ] . parseError ) . toBeUndefined ( ) ;
160+
161+ expect ( result [ 2 ] . parseError ) . toBe ( 'shutdown' ) ;
162+ expect ( result [ 2 ] . cancelled ) . toBe ( false ) ;
163+ } , 15_000 ) ;
164+ } ,
165+ ) ;
0 commit comments