@@ -44,6 +44,78 @@ describe("TelemetryService.shutdown draining", () => {
4444 expect ( captureOrder ) . toEqual ( [ "captured" , "shutdown" ] )
4545 } )
4646
47+ it ( "drains a second capture that gets queued after shutdown() has already started draining" , async ( ) => {
48+ // Regression test: shutdown() must not take a single Promise.all snapshot of
49+ // pendingClientCalls. A promise added to pendingClientCalls *after* Promise.all(set)
50+ // has already been called is never awaited by that call, even if it never resolves
51+ // -- Promise.all takes its list of promises to await synchronously, at call time.
52+ // So a second capture queued while the first Promise.all pass is still pending (e.g.
53+ // a teardown-time error handler reacting to something unrelated) would be silently
54+ // dropped by a single-pass drain, letting client.shutdown() run without it.
55+ let resolveFirstCapture ! : ( ) => void
56+ const firstCapturePromise = new Promise < void > ( ( resolve ) => {
57+ resolveFirstCapture = resolve
58+ } )
59+
60+ let resolveSecondCapture ! : ( ) => void
61+ const secondCapturePromise = new Promise < void > ( ( resolve ) => {
62+ resolveSecondCapture = resolve
63+ } )
64+
65+ const captureOrder : string [ ] = [ ]
66+ let firstCaptureStarted = false
67+
68+ const mockClient : TelemetryClient = {
69+ setProvider : vi . fn ( ) ,
70+ capture : vi . fn ( ) . mockImplementation ( async ( ) => {
71+ if ( ! firstCaptureStarted ) {
72+ firstCaptureStarted = true
73+ await firstCapturePromise
74+ captureOrder . push ( "first-captured" )
75+ return
76+ }
77+
78+ await secondCapturePromise
79+ captureOrder . push ( "second-captured" )
80+ } ) ,
81+ captureException : vi . fn ( ) ,
82+ updateTelemetryState : vi . fn ( ) ,
83+ isTelemetryEnabled : vi . fn ( ) . mockReturnValue ( true ) ,
84+ shutdown : vi . fn ( ) . mockImplementation ( async ( ) => {
85+ captureOrder . push ( "shutdown" )
86+ } ) ,
87+ }
88+
89+ const service = new TelemetryService ( [ mockClient ] )
90+
91+ service . captureEvent ( TelemetryEventName . TASK_CREATED , { taskId : "first" } )
92+
93+ const shutdownPromise = service . shutdown ( )
94+
95+ // Queue the second capture synchronously, immediately after shutdown() has started
96+ // (and thus after its first Promise.all(pendingClientCalls) pass has already taken
97+ // its snapshot). This is the scenario the loop fix protects against.
98+ service . captureEvent ( TelemetryEventName . TASK_CREATED , { taskId : "second" } )
99+
100+ resolveFirstCapture ( )
101+ // Flush several microtask ticks so a buggy single-pass drain has every opportunity
102+ // to run client.shutdown() before we check -- a couple of ticks isn't enough since
103+ // the mocked capture/shutdown chain itself spans a few microtask hops.
104+ for ( let i = 0 ; i < 4 ; i ++ ) {
105+ await Promise . resolve ( )
106+ }
107+
108+ // The second capture is still pending (its own resolver hasn't fired) -- shutdown
109+ // must not have completed yet, otherwise it dropped the second capture.
110+ expect ( captureOrder ) . not . toContain ( "shutdown" )
111+
112+ resolveSecondCapture ( )
113+ await shutdownPromise
114+
115+ expect ( mockClient . capture ) . toHaveBeenCalledTimes ( 2 )
116+ expect ( captureOrder ) . toEqual ( [ "first-captured" , "second-captured" , "shutdown" ] )
117+ } )
118+
47119 it ( "does not let a rejected capture prevent shutdown from completing" , async ( ) => {
48120 const mockClient : TelemetryClient = {
49121 setProvider : vi . fn ( ) ,
0 commit comments