1- import { describe , expect , it } from 'vitest' ;
1+ import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
22import * as lzma from '../../src/lzma.js' ;
33
44describe ( 'Error Recovery and State Transitions' , ( ) => {
5+ // Track every stream + timer created so we can guarantee cleanup, even if a
6+ // test rejects mid-flight or a native callback is queued after the test ends.
7+ // Without this, late callbacks (xz.emit('onerror', ...) firing after close())
8+ // can crash the vitest worker — historical Windows+Node20 / Ubuntu+Node24 flakes.
9+ let streams : lzma . Xz [ ] = [ ] ;
10+ let timers : NodeJS . Timeout [ ] = [ ] ;
11+
12+ const track = < T extends lzma . Xz > ( s : T ) : T => {
13+ streams . push ( s ) ;
14+ return s ;
15+ } ;
16+
17+ const trackTimer = ( t : NodeJS . Timeout ) : NodeJS . Timeout => {
18+ timers . push ( t ) ;
19+ return t ;
20+ } ;
21+
22+ beforeEach ( ( ) => {
23+ streams = [ ] ;
24+ timers = [ ] ;
25+ } ) ;
26+
27+ afterEach ( ( ) => {
28+ for ( const t of timers ) clearTimeout ( t ) ;
29+ for ( const s of streams ) {
30+ try {
31+ if ( typeof s . destroy === 'function' ) s . destroy ( ) ;
32+ else if ( typeof s . close === 'function' && ! s . _closed ) s . close ( ) ;
33+ } catch {
34+ // best-effort: ignore double-destroy errors
35+ }
36+ }
37+ } ) ;
38+
539 it ( 'should cover filter validation catch block' , ( ) => {
640 // Force the try-catch path in filter validation
741 expect ( ( ) => {
@@ -14,12 +48,13 @@ describe('Error Recovery and State Transitions', () => {
1448 } ;
1549
1650 const xz = new lzma . Xz ( { filters : malformedFilters } ) ;
51+ track ( xz ) ;
1752 xz . close ( ) ;
1853 } ) . toThrow ( 'Filters need to be in an array!' ) ;
1954 } ) ;
2055
2156 it ( 'should cover callback when stream is ending' , async ( ) => {
22- const xz = new lzma . Xz ( ) ;
57+ const xz = track ( new lzma . Xz ( ) ) ;
2358
2459 return new Promise < void > ( ( resolve ) => {
2560 let callbackExecuted = false ;
@@ -29,27 +64,27 @@ describe('Error Recovery and State Transitions', () => {
2964 xz . end ( ) ; // This puts stream in 'ending' state
3065
3166 // Immediately try to flush while ending - should trigger the appropriate behavior
32- setTimeout ( ( ) => {
33- xz . flush ( ( ) => {
34- callbackExecuted = true ;
35- expect ( callbackExecuted ) . toBe ( true ) ;
36- xz . close ( ) ;
37- resolve ( ) ;
38- } ) ;
39- } , 1 ) ; // Very small delay to ensure ending state
67+ trackTimer (
68+ setTimeout ( ( ) => {
69+ xz . flush ( ( ) => {
70+ callbackExecuted = true ;
71+ expect ( callbackExecuted ) . toBe ( true ) ;
72+ resolve ( ) ;
73+ } ) ;
74+ } , 1 ) // Very small delay to ensure ending state
75+ ) ;
4076
4177 // Fallback timeout
42- setTimeout ( ( ) => {
43- if ( ! callbackExecuted ) {
44- xz . close ( ) ;
45- resolve ( ) ;
46- }
47- } , 50 ) ;
78+ trackTimer (
79+ setTimeout ( ( ) => {
80+ if ( ! callbackExecuted ) resolve ( ) ;
81+ } , 50 )
82+ ) ;
4883 } ) ;
4984 } ) ;
5085
5186 it ( 'should cover lines 457-458 - async LZMA error handling' , async ( ) => {
52- const xz = new lzma . Xz ( ) ;
87+ const xz = track ( new lzma . Xz ( ) ) ;
5388
5489 return new Promise < void > ( ( resolve ) => {
5590 let errorCount = 0 ;
@@ -59,16 +94,13 @@ describe('Error Recovery and State Transitions', () => {
5994 expect ( error ) . toBeInstanceOf ( Error ) ;
6095 // Accept any LZMA error code (native + manual emit may both fire)
6196 expect ( error . errno ) . toBeGreaterThan ( 0 ) ;
62-
63- if ( errorCount === 1 ) {
64- xz . close ( ) ;
65- }
97+ if ( errorCount === 1 ) xz . close ( ) ;
6698 } ) ;
6799
68100 // Absorb any errors after stream is destroyed (e.g. LZMA_PROG_ERROR
69101 // from native callback firing after close)
70102 xz . on ( 'close' , ( ) => {
71- setTimeout ( resolve , 50 ) ;
103+ trackTimer ( setTimeout ( resolve , 50 ) ) ;
72104 } ) ;
73105
74106 // Emit onerror directly — this triggers the onerror→error conversion (line 357-360)
@@ -86,7 +118,7 @@ describe('Error Recovery and State Transitions', () => {
86118 ] ;
87119
88120 for ( const errorCode of testCases ) {
89- const xz = new lzma . Xz ( ) ;
121+ const xz = track ( new lzma . Xz ( ) ) ;
90122
91123 await new Promise < void > ( ( resolve ) => {
92124 xz . on ( 'error' , ( error ) => {
@@ -102,18 +134,15 @@ describe('Error Recovery and State Transitions', () => {
102134 } ) ;
103135
104136 it ( 'should handle stream state transitions correctly' , async ( ) => {
105- const xz = new lzma . Xz ( ) ;
137+ const xz = track ( new lzma . Xz ( ) ) ;
106138
107139 return new Promise < void > ( ( resolve ) => {
108140 let statesTracked = 0 ;
109141 const expectedStates = 3 ;
110142
111143 const checkComplete = ( ) => {
112144 statesTracked ++ ;
113- if ( statesTracked >= expectedStates ) {
114- xz . close ( ) ;
115- resolve ( ) ;
116- }
145+ if ( statesTracked >= expectedStates ) resolve ( ) ;
117146 } ;
118147
119148 // Test ended state flush
@@ -126,9 +155,11 @@ describe('Error Recovery and State Transitions', () => {
126155 xz . end ( ) ;
127156
128157 // Test normal flush
129- setTimeout ( ( ) => {
130- xz . flush ( ( ) => checkComplete ( ) ) ;
131- } , 5 ) ;
158+ trackTimer (
159+ setTimeout ( ( ) => {
160+ xz . flush ( ( ) => checkComplete ( ) ) ;
161+ } , 5 )
162+ ) ;
132163
133164 checkComplete ( ) ; // Initial count
134165 } ) ;
0 commit comments