@@ -20,6 +20,9 @@ jest.mock("vscode", () => ({
2020 ThemeIcon : jest . fn ( ) ,
2121} ) )
2222
23+ const TERMINAL_OUTPUT_LIMIT = 100 * 1024
24+ const STALL_TIMEOUT = 100
25+
2326describe ( "TerminalProcess" , ( ) => {
2427 let terminalProcess : TerminalProcess
2528 let mockTerminal : jest . Mocked <
@@ -34,7 +37,7 @@ describe("TerminalProcess", () => {
3437 let mockStream : AsyncIterableIterator < string >
3538
3639 beforeEach ( ( ) => {
37- terminalProcess = new TerminalProcess ( 100 * 1024 )
40+ terminalProcess = new TerminalProcess ( TERMINAL_OUTPUT_LIMIT , STALL_TIMEOUT )
3841
3942 // Create properly typed mock terminal
4043 mockTerminal = {
@@ -173,4 +176,156 @@ describe("TerminalProcess", () => {
173176 expect ( terminalProcess [ "isListening" ] ) . toBe ( false )
174177 } )
175178 } )
179+
180+ describe ( "stalled stream handling" , ( ) => {
181+ it ( "emits stream_stalled event when no output is received within timeout" , async ( ) => {
182+ // Create a promise that resolves when stream_stalled is emitted
183+ const streamStalledPromise = new Promise < number > ( ( resolve ) => {
184+ terminalProcess . once ( "stream_stalled" , ( id : number ) => {
185+ resolve ( id )
186+ } )
187+ } )
188+
189+ // Create a stream that doesn't emit any data
190+ mockStream = ( async function * ( ) {
191+ yield "\x1b]633;C\x07" // Command start sequence
192+ // No data is yielded after this, causing the stall
193+ await new Promise ( ( resolve ) => setTimeout ( resolve , STALL_TIMEOUT * 2 ) )
194+ // This would normally be yielded, but the stall timer will fire first
195+ yield "Output after stall"
196+ yield "\x1b]633;D\x07" // Command end sequence
197+ terminalProcess . emit ( "shell_execution_complete" , mockTerminalInfo . id , { exitCode : 0 } )
198+ } ) ( )
199+
200+ mockExecution = {
201+ read : jest . fn ( ) . mockReturnValue ( mockStream ) ,
202+ }
203+
204+ mockTerminal . shellIntegration . executeCommand . mockReturnValue ( mockExecution )
205+
206+ // Start the terminal process
207+ const runPromise = terminalProcess . run ( mockTerminal , "test command" )
208+ terminalProcess . emit ( "stream_available" , mockTerminalInfo . id , mockStream )
209+
210+ // Wait for the stream_stalled event
211+ const stalledId = await streamStalledPromise
212+
213+ // Verify the event was emitted with the correct terminal ID
214+ expect ( stalledId ) . toBe ( mockTerminalInfo . id )
215+
216+ // Complete the run
217+ await runPromise
218+ } )
219+
220+ it ( "clears stall timer when output is received" , async ( ) => {
221+ // Spy on the emit method to check if stream_stalled is emitted
222+ const emitSpy = jest . spyOn ( terminalProcess , "emit" )
223+
224+ // Create a stream that emits data before the stall timeout
225+ mockStream = ( async function * ( ) {
226+ yield "\x1b]633;C\x07" // Command start sequence
227+ yield "Initial output\n" // This should clear the stall timer
228+
229+ // Wait longer than the stall timeout
230+ await new Promise ( ( resolve ) => setTimeout ( resolve , STALL_TIMEOUT * 2 ) )
231+
232+ yield "More output\n"
233+ yield "\x1b]633;D\x07" // Command end sequence
234+ terminalProcess . emit ( "shell_execution_complete" , mockTerminalInfo . id , { exitCode : 0 } )
235+ } ) ( )
236+
237+ mockExecution = {
238+ read : jest . fn ( ) . mockReturnValue ( mockStream ) ,
239+ }
240+
241+ mockTerminal . shellIntegration . executeCommand . mockReturnValue ( mockExecution )
242+
243+ // Start the terminal process
244+ const runPromise = terminalProcess . run ( mockTerminal , "test command" )
245+ terminalProcess . emit ( "stream_available" , mockTerminalInfo . id , mockStream )
246+
247+ // Wait for the run to complete
248+ await runPromise
249+
250+ // Wait a bit longer to ensure the stall timer would have fired if not cleared
251+ await new Promise ( ( resolve ) => setTimeout ( resolve , STALL_TIMEOUT * 2 ) )
252+
253+ // Verify stream_stalled was not emitted
254+ expect ( emitSpy ) . not . toHaveBeenCalledWith ( "stream_stalled" , expect . anything ( ) )
255+ } )
256+
257+ it ( "returns true from flushLine when a line is emitted" , async ( ) => {
258+ // Create a stream with output
259+ mockStream = ( async function * ( ) {
260+ yield "\x1b]633;C\x07" // Command start sequence
261+ yield "Test output\n" // This should be flushed as a line
262+ yield "\x1b]633;D\x07" // Command end sequence
263+ terminalProcess . emit ( "shell_execution_complete" , mockTerminalInfo . id , { exitCode : 0 } )
264+ } ) ( )
265+
266+ mockExecution = {
267+ read : jest . fn ( ) . mockReturnValue ( mockStream ) ,
268+ }
269+
270+ mockTerminal . shellIntegration . executeCommand . mockReturnValue ( mockExecution )
271+
272+ // Spy on the flushLine method
273+ const flushLineSpy = jest . spyOn ( terminalProcess as any , "flushLine" )
274+
275+ // Spy on the emit method to check if line is emitted
276+ const emitSpy = jest . spyOn ( terminalProcess , "emit" )
277+
278+ // Start the terminal process
279+ const runPromise = terminalProcess . run ( mockTerminal , "test command" )
280+ terminalProcess . emit ( "stream_available" , mockTerminalInfo . id , mockStream )
281+
282+ // Wait for the run to complete
283+ await runPromise
284+
285+ // Verify flushLine was called and returned true
286+ expect ( flushLineSpy ) . toHaveBeenCalled ( )
287+ expect ( flushLineSpy . mock . results . some ( ( result ) => result . value === true ) ) . toBe ( true )
288+
289+ // Verify line event was emitted
290+ expect ( emitSpy ) . toHaveBeenCalledWith ( "line" , expect . any ( String ) )
291+ } )
292+
293+ it ( "returns false from flushLine when no line is emitted" , async ( ) => {
294+ // Create a stream with no complete lines
295+ mockStream = ( async function * ( ) {
296+ yield "\x1b]633;C\x07" // Command start sequence
297+ yield "Test output" // No newline, so this won't be flushed as a line yet
298+ yield "\x1b]633;D\x07" // Command end sequence
299+ terminalProcess . emit ( "shell_execution_complete" , mockTerminalInfo . id , { exitCode : 0 } )
300+ } ) ( )
301+
302+ mockExecution = {
303+ read : jest . fn ( ) . mockReturnValue ( mockStream ) ,
304+ }
305+
306+ mockTerminal . shellIntegration . executeCommand . mockReturnValue ( mockExecution )
307+
308+ // Create a custom implementation to test flushLine directly
309+ const testFlushLine = async ( ) => {
310+ // Create a new instance with the same configuration
311+ const testProcess = new TerminalProcess ( TERMINAL_OUTPUT_LIMIT , STALL_TIMEOUT )
312+
313+ // Set up the output builder with content that doesn't have a newline
314+ testProcess [ "outputBuilder" ] = {
315+ readLine : jest . fn ( ) . mockReturnValue ( "" ) ,
316+ append : jest . fn ( ) ,
317+ reset : jest . fn ( ) ,
318+ content : "Test output" ,
319+ } as any
320+
321+ // Call flushLine directly
322+ const result = testProcess [ "flushLine" ] ( )
323+ return result
324+ }
325+
326+ // Test flushLine directly
327+ const flushLineResult = await testFlushLine ( )
328+ expect ( flushLineResult ) . toBe ( false )
329+ } )
330+ } )
176331} )
0 commit comments