@@ -8,6 +8,7 @@ import { formatResponse } from "../../prompts/responses"
88import { ToolUse , AskApproval , HandleError , PushToolResult } from "../../../shared/tools"
99import { unescapeHtmlEntities } from "../../../utils/text-normalization"
1010import { Terminal } from "../../../integrations/terminal/Terminal"
11+ import type { RooTerminalCallbacks , RooTerminalProcess } from "../../../integrations/terminal/types"
1112
1213// Mock dependencies
1314vitest . mock ( "execa" , ( ) => ( {
@@ -333,4 +334,109 @@ describe("executeCommandTool", () => {
333334 expect ( executeCommandModule . resolveAgentTimeoutMs ( 30 ) ) . toBe ( 30_000 )
334335 } )
335336 } )
337+
338+ describe ( "command_output ask policy" , ( ) => {
339+ type MockProcess = Promise < void > & {
340+ continue : ReturnType < typeof vitest . fn >
341+ abort : ReturnType < typeof vitest . fn >
342+ }
343+
344+ interface ControllableTerminal {
345+ callbacks : RooTerminalCallbacks | undefined
346+ proc : MockProcess
347+ resolveProcess : ( ) => void
348+ }
349+
350+ const setupControllableTerminal = async ( ) : Promise < ControllableTerminal > => {
351+ const { TerminalRegistry } = await import ( "../../../integrations/terminal/TerminalRegistry" )
352+ const state : ControllableTerminal = {
353+ callbacks : undefined ,
354+ proc : undefined as unknown as MockProcess ,
355+ resolveProcess : ( ) => { } ,
356+ }
357+ const processPromise = new Promise < void > ( ( resolve ) => {
358+ state . resolveProcess = resolve
359+ } )
360+ state . proc = Object . assign ( processPromise , { continue : vitest . fn ( ) , abort : vitest . fn ( ) } )
361+ ; ( TerminalRegistry . getOrCreateTerminal as ReturnType < typeof vitest . fn > ) . mockResolvedValue ( {
362+ runCommand : vitest . fn ( ( _cmd : string , callbacks : RooTerminalCallbacks ) => {
363+ state . callbacks = callbacks
364+ return state . proc
365+ } ) ,
366+ getCurrentWorkingDirectory : vitest . fn ( ) . mockReturnValue ( "/test/workspace" ) ,
367+ } )
368+ return state
369+ }
370+
371+ const handleCommand = ( command : string ) => {
372+ mockToolUse . params . command = command
373+ mockToolUse . nativeArgs = { command }
374+
375+ return executeCommandTool . handle ( mockCline as unknown as Task , mockToolUse , {
376+ askApproval : mockAskApproval as unknown as AskApproval ,
377+ handleError : mockHandleError as unknown as HandleError ,
378+ pushToolResult : mockPushToolResult as unknown as PushToolResult ,
379+ } )
380+ }
381+
382+ it ( "does not ask about command output when a short command emits output and exits normally" , async ( ) => {
383+ vitest . useFakeTimers ( )
384+ const terminal = await setupControllableTerminal ( )
385+
386+ const handlePromise = handleCommand ( "echo hello" )
387+
388+ await vitest . waitFor ( ( ) => expect ( terminal . callbacks ) . toBeDefined ( ) )
389+ const callbacks = terminal . callbacks !
390+ const proc = terminal . proc as unknown as RooTerminalProcess
391+
392+ await callbacks . onLine ( "hello\n" , proc )
393+ await callbacks . onCompleted ! ( "hello\n" , proc )
394+ callbacks . onShellExecutionComplete ! ( { exitCode : 0 } , proc )
395+ terminal . resolveProcess ( )
396+
397+ // Advance past the ask delay to prove the scheduled ask was cancelled
398+ // on completion, not merely deferred beyond the test's runtime.
399+ await vitest . advanceTimersByTimeAsync ( executeCommandModule . COMMAND_OUTPUT_ASK_DELAY_MS + 1_000 )
400+
401+ await handlePromise
402+
403+ expect ( mockCline . ask ) . not . toHaveBeenCalled ( )
404+ expect ( mockPushToolResult ) . toHaveBeenCalled ( )
405+ const result = mockPushToolResult . mock . calls [ 0 ] [ 0 ]
406+ expect ( result ) . toContain ( "hello" )
407+ expect ( result ) . toContain ( "Exit code: 0" )
408+ } )
409+
410+ it ( "asks about command output when the command is still running after the ask delay" , async ( ) => {
411+ vitest . useFakeTimers ( )
412+ mockCline . ask . mockResolvedValue ( { response : "messageResponse" , text : "keep going" , images : undefined } )
413+ const terminal = await setupControllableTerminal ( )
414+
415+ const handlePromise = handleCommand ( "sleep 60" )
416+
417+ await vitest . waitFor ( ( ) => expect ( terminal . callbacks ) . toBeDefined ( ) )
418+ const callbacks = terminal . callbacks !
419+ const proc = terminal . proc as unknown as RooTerminalProcess
420+
421+ await callbacks . onLine ( "working...\n" , proc )
422+
423+ // First output alone must not trigger the ask.
424+ expect ( mockCline . ask ) . not . toHaveBeenCalled ( )
425+
426+ await vitest . advanceTimersByTimeAsync ( executeCommandModule . COMMAND_OUTPUT_ASK_DELAY_MS )
427+
428+ expect ( mockCline . ask ) . toHaveBeenCalledWith ( "command_output" , "" )
429+ expect ( terminal . proc . continue ) . toHaveBeenCalled ( )
430+
431+ // Let the command finish so the tool can resolve.
432+ await callbacks . onCompleted ! ( "working...\n" , proc )
433+ callbacks . onShellExecutionComplete ! ( { exitCode : 0 } , proc )
434+ terminal . resolveProcess ( )
435+ await vitest . advanceTimersByTimeAsync ( 100 )
436+
437+ await handlePromise
438+
439+ expect ( mockPushToolResult ) . toHaveBeenCalled ( )
440+ } )
441+ } )
336442} )
0 commit comments