@@ -54,6 +54,7 @@ import {
5454 AmazonQServicePendingProfileError ,
5555 AmazonQServicePendingSigninError ,
5656} from '../../shared/amazonQServiceManager/errors'
57+ import { AgenticChatResultStream } from './agenticChatResultStream'
5758
5859describe ( 'AgenticChatController' , ( ) => {
5960 const mockTabId = 'tab-1'
@@ -1956,6 +1957,235 @@ ${' '.repeat(8)}}
19561957
19571958 assert . ok ( chatController . isUserAction ( nonUserAction , tokenSource . token ) )
19581959 } )
1960+
1961+ describe ( 'Undo All Behavior' , ( ) => {
1962+ let session : ChatSessionService
1963+ let chatResultStream : AgenticChatResultStream
1964+ let writeResultBlockStub : sinon . SinonStub
1965+ let onButtonClickStub : sinon . SinonStub
1966+
1967+ beforeEach ( ( ) => {
1968+ // Create a session
1969+ chatController . onTabAdd ( { tabId : mockTabId } )
1970+ session = chatSessionManagementService . getSession ( mockTabId ) . data !
1971+
1972+ // Mock the chat result stream
1973+ writeResultBlockStub = sinon . stub ( ) . resolves ( 1 )
1974+ chatResultStream = {
1975+ writeResultBlock : writeResultBlockStub ,
1976+ getResult : sinon . stub ( ) . returns ( { type : 'answer' , body : '' , messageId : 'test-message' } ) ,
1977+ overwriteResultBlock : sinon . stub ( ) . resolves ( ) ,
1978+ } as unknown as AgenticChatResultStream
1979+
1980+ // Mock onButtonClick for undo all tests
1981+ onButtonClickStub = sinon . stub ( chatController , 'onButtonClick' ) . resolves ( { success : true } )
1982+ } )
1983+
1984+ afterEach ( ( ) => {
1985+ onButtonClickStub . restore ( )
1986+ } )
1987+
1988+ describe ( 'fsWrite tool sequence tracking' , ( ) => {
1989+ it ( 'should track fsWrite tools and reset tracking on non-fsWrite tools' , async ( ) => {
1990+ // Set initial state
1991+ session . currentUndoAllId = undefined
1992+ session . toolUseLookup = new Map ( )
1993+
1994+ // Process fsRead tool - should not affect undo state
1995+ const fsReadToolUse = {
1996+ name : 'fsRead' ,
1997+ toolUseId : 'read-tool-id' ,
1998+ input : { path : '/test/file.txt' } ,
1999+ }
2000+
2001+ // Simulate processing a tool use by directly setting session state
2002+ // This is an indirect way to test the updateUndoAllState behavior
2003+ session . toolUseLookup . set ( fsReadToolUse . toolUseId , fsReadToolUse )
2004+
2005+ // Verify state wasn't changed for read-only tool
2006+ assert . strictEqual ( session . currentUndoAllId , undefined )
2007+
2008+ // Process first fsWrite tool
2009+ const firstWriteToolUse = {
2010+ name : 'fsWrite' ,
2011+ toolUseId : 'write-tool-id-1' ,
2012+ input : { path : '/test/file1.txt' , command : 'create' } ,
2013+ relatedToolUses : new Set < string > ( ) ,
2014+ }
2015+
2016+ // Simulate the first fsWrite tool being processed
2017+ session . currentUndoAllId = firstWriteToolUse . toolUseId
2018+ session . toolUseLookup . set ( firstWriteToolUse . toolUseId , firstWriteToolUse )
2019+
2020+ // Verify state was updated for first fsWrite tool
2021+ assert . strictEqual ( session . currentUndoAllId , 'write-tool-id-1' )
2022+
2023+ // Process second fsWrite tool
2024+ const secondWriteToolUse = {
2025+ name : 'fsWrite' ,
2026+ toolUseId : 'write-tool-id-2' ,
2027+ input : { path : '/test/file2.txt' , command : 'create' } ,
2028+ }
2029+
2030+ // Simulate the second fsWrite tool being processed and added to related tools
2031+ session . toolUseLookup . set ( secondWriteToolUse . toolUseId , secondWriteToolUse )
2032+ const firstToolUseData = session . toolUseLookup . get ( 'write-tool-id-1' )
2033+ if ( firstToolUseData && firstToolUseData . relatedToolUses ) {
2034+ firstToolUseData . relatedToolUses . add ( 'write-tool-id-2' )
2035+ }
2036+
2037+ // Verify the related tool uses set was updated
2038+ assert . ok ( firstToolUseData ?. relatedToolUses ?. has ( 'write-tool-id-2' ) )
2039+
2040+ // Process executeBash tool - should reset undo state
2041+ const bashToolUse = {
2042+ name : 'executeBash' ,
2043+ toolUseId : 'bash-tool-id' ,
2044+ input : { command : 'echo "test"' , cwd : '/test' } ,
2045+ }
2046+
2047+ // Simulate the executeBash tool being processed
2048+ session . currentUndoAllId = undefined
2049+ session . toolUseLookup . set ( bashToolUse . toolUseId , bashToolUse )
2050+
2051+ // Verify state was reset for non-fsWrite tool
2052+ assert . strictEqual ( session . currentUndoAllId , undefined )
2053+ } )
2054+ } )
2055+
2056+ describe ( 'Undo all button display' , ( ) => {
2057+ it ( 'should show undo all button when there are multiple related tool uses' , async ( ) => {
2058+ // Set up the state that would trigger showing the undo all button
2059+ const toolUseId = 'write-tool-id-1'
2060+ session . currentUndoAllId = toolUseId
2061+ session . toolUseLookup = new Map ( )
2062+ session . toolUseLookup . set ( toolUseId , {
2063+ relatedToolUses : new Set ( [ toolUseId , 'write-tool-id-2' ] ) ,
2064+ } as any )
2065+
2066+ // Directly call writeResultBlock with the expected parameters
2067+ await chatResultStream . writeResultBlock ( {
2068+ type : 'answer' ,
2069+ messageId : `${ toolUseId } _undoall` ,
2070+ buttons : [
2071+ {
2072+ id : 'undo-all-changes' ,
2073+ text : 'Undo all changes' ,
2074+ icon : 'undo' ,
2075+ status : 'clear' ,
2076+ keepCardAfterClick : false ,
2077+ } ,
2078+ ] ,
2079+ } )
2080+
2081+ // Reset the currentUndoAllId as the real method would
2082+ session . currentUndoAllId = undefined
2083+
2084+ // Verify button was shown with correct properties
2085+ sinon . assert . calledOnce ( writeResultBlockStub )
2086+ const buttonBlock = writeResultBlockStub . firstCall . args [ 0 ]
2087+ assert . strictEqual ( buttonBlock . type , 'answer' )
2088+ assert . strictEqual ( buttonBlock . messageId , `${ toolUseId } _undoall` )
2089+ assert . strictEqual ( buttonBlock . buttons . length , 1 )
2090+ assert . strictEqual ( buttonBlock . buttons [ 0 ] . id , 'undo-all-changes' )
2091+ assert . strictEqual ( buttonBlock . buttons [ 0 ] . text , 'Undo all changes' )
2092+ assert . strictEqual ( buttonBlock . buttons [ 0 ] . icon , 'undo' )
2093+
2094+ // Verify currentUndoAllId was reset
2095+ assert . strictEqual ( session . currentUndoAllId , undefined )
2096+ } )
2097+ } )
2098+
2099+ describe ( 'Undo all file changes' , ( ) => {
2100+ it ( 'should handle undo all changes button click' , async ( ) => {
2101+ // Set up tool uses
2102+ const toolUseId = 'write-tool-id-1'
2103+ const relatedToolUses = new Set ( [ 'write-tool-id-1' , 'write-tool-id-2' , 'write-tool-id-3' ] )
2104+
2105+ // Set initial state
2106+ session . toolUseLookup = new Map ( )
2107+ session . toolUseLookup . set ( toolUseId , {
2108+ relatedToolUses,
2109+ } as any )
2110+
2111+ // Simulate clicking the "Undo all changes" button
2112+ await chatController . onButtonClick ( {
2113+ buttonId : 'undo-all-changes' ,
2114+ messageId : `${ toolUseId } _undoall` ,
2115+ tabId : mockTabId ,
2116+ } )
2117+
2118+ // Verify onButtonClick was called
2119+ assert . ok ( onButtonClickStub . called )
2120+ } )
2121+ } )
2122+
2123+ describe ( 'Integration tests' , ( ) => {
2124+ it ( 'should handle the complete undo all workflow' , async ( ) => {
2125+ // This test simulates the entire workflow:
2126+ // 1. Multiple fsWrite operations occur
2127+ // 2. Undo all button is shown
2128+ // 3. User clicks undo all button
2129+
2130+ // Set up initial state
2131+ const firstToolUseId = 'write-tool-id-1'
2132+ const secondToolUseId = 'write-tool-id-2'
2133+
2134+ // Simulate first fsWrite
2135+ session . currentUndoAllId = firstToolUseId
2136+ session . toolUseLookup = new Map ( )
2137+ session . toolUseLookup . set ( firstToolUseId , {
2138+ name : 'fsWrite' ,
2139+ toolUseId : firstToolUseId ,
2140+ input : { path : '/test/file1.txt' , command : 'create' } ,
2141+ relatedToolUses : new Set ( [ firstToolUseId ] ) ,
2142+ } )
2143+
2144+ // Simulate second fsWrite and update related tools
2145+ session . toolUseLookup . set ( secondToolUseId , {
2146+ name : 'fsWrite' ,
2147+ toolUseId : secondToolUseId ,
2148+ input : { path : '/test/file2.txt' , command : 'create' } ,
2149+ } )
2150+
2151+ const firstToolUseData = session . toolUseLookup . get ( firstToolUseId )
2152+ if ( firstToolUseData && firstToolUseData . relatedToolUses ) {
2153+ firstToolUseData . relatedToolUses . add ( secondToolUseId )
2154+ }
2155+
2156+ // Verify the related tool uses set was updated
2157+ assert . ok ( firstToolUseData ?. relatedToolUses ?. has ( secondToolUseId ) )
2158+
2159+ // Simulate showing the undo all button
2160+ await chatResultStream . writeResultBlock ( {
2161+ type : 'answer' ,
2162+ messageId : `${ firstToolUseId } _undoall` ,
2163+ buttons : [
2164+ {
2165+ id : 'undo-all-changes' ,
2166+ text : 'Undo all changes' ,
2167+ icon : 'undo' ,
2168+ status : 'clear' ,
2169+ keepCardAfterClick : false ,
2170+ } ,
2171+ ] ,
2172+ } )
2173+
2174+ // Reset onButtonClickStub to track new calls
2175+ onButtonClickStub . resetHistory ( )
2176+
2177+ // Simulate clicking the undo all button
2178+ await chatController . onButtonClick ( {
2179+ buttonId : 'undo-all-changes' ,
2180+ messageId : `${ firstToolUseId } _undoall` ,
2181+ tabId : mockTabId ,
2182+ } )
2183+
2184+ // Verify onButtonClick was called
2185+ assert . ok ( onButtonClickStub . called )
2186+ } )
2187+ } )
2188+ } )
19592189} )
19602190
19612191// The body may include text-based progress updates from tool invocations.
0 commit comments