@@ -148,6 +148,18 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({
148148
149149vi . mock ( "../../ignore/RooIgnoreController" )
150150
151+ vi . mock ( "../../condense" , async ( importOriginal ) => {
152+ const actual = ( await importOriginal ( ) ) as any
153+ return {
154+ ...actual ,
155+ summarizeConversation : vi . fn ( ) . mockResolvedValue ( {
156+ messages : [ { role : "user" , content : [ { type : "text" , text : "continued" } ] , ts : Date . now ( ) } ] ,
157+ summary : "summary" ,
158+ cost : 0 ,
159+ newContextTokens : 1 ,
160+ } ) ,
161+ }
162+ } )
151163// Mock storagePathManager to prevent dynamic import issues.
152164vi . mock ( "../../../utils/storage" , ( ) => ( {
153165 getTaskDirectoryPath : vi
@@ -1777,3 +1789,124 @@ describe("Cline", () => {
17771789 } )
17781790 } )
17791791} )
1792+
1793+ describe ( "Queued message processing after condense" , ( ) => {
1794+ function createProvider ( ) : any {
1795+ const storageUri = { fsPath : path . join ( os . tmpdir ( ) , "test-storage" ) }
1796+ const ctx = {
1797+ globalState : {
1798+ get : vi . fn ( ) . mockImplementation ( ( _key : keyof GlobalState ) => undefined ) ,
1799+ update : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1800+ keys : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1801+ } ,
1802+ globalStorageUri : storageUri ,
1803+ workspaceState : {
1804+ get : vi . fn ( ) . mockImplementation ( ( _key ) => undefined ) ,
1805+ update : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1806+ keys : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1807+ } ,
1808+ secrets : {
1809+ get : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1810+ store : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1811+ delete : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1812+ } ,
1813+ extensionUri : { fsPath : "/mock/extension/path" } ,
1814+ extension : { packageJSON : { version : "1.0.0" } } ,
1815+ } as unknown as vscode . ExtensionContext
1816+
1817+ const output = {
1818+ appendLine : vi . fn ( ) ,
1819+ append : vi . fn ( ) ,
1820+ clear : vi . fn ( ) ,
1821+ show : vi . fn ( ) ,
1822+ hide : vi . fn ( ) ,
1823+ dispose : vi . fn ( ) ,
1824+ }
1825+
1826+ const provider = new ClineProvider ( ctx , output as any , "sidebar" , new ContextProxy ( ctx ) ) as any
1827+ provider . postMessageToWebview = vi . fn ( ) . mockResolvedValue ( undefined )
1828+ provider . postStateToWebview = vi . fn ( ) . mockResolvedValue ( undefined )
1829+ provider . getState = vi . fn ( ) . mockResolvedValue ( { } )
1830+ return provider
1831+ }
1832+
1833+ const apiConfig : ProviderSettings = {
1834+ apiProvider : "anthropic" ,
1835+ apiModelId : "claude-3-5-sonnet-20241022" ,
1836+ apiKey : "test-api-key" ,
1837+ } as any
1838+
1839+ it ( "processes queued message after condense completes" , async ( ) => {
1840+ const provider = createProvider ( )
1841+ const task = new Task ( {
1842+ provider,
1843+ apiConfiguration : apiConfig ,
1844+ task : "initial task" ,
1845+ startTask : false ,
1846+ } )
1847+
1848+ // Make condense fast + deterministic
1849+ vi . spyOn ( task as any , "getSystemPrompt" ) . mockResolvedValue ( "system" )
1850+ const submitSpy = vi . spyOn ( task , "submitUserMessage" ) . mockResolvedValue ( undefined )
1851+
1852+ // Queue a message during condensing
1853+ task . messageQueueService . addMessage ( "queued text" , [ "img1.png" ] )
1854+
1855+ // Use fake timers to capture setTimeout(0) in processQueuedMessages
1856+ vi . useFakeTimers ( )
1857+ await task . condenseContext ( )
1858+
1859+ // Flush the microtask that submits the queued message
1860+ vi . runAllTimers ( )
1861+ vi . useRealTimers ( )
1862+
1863+ expect ( submitSpy ) . toHaveBeenCalledWith ( "queued text" , [ "img1.png" ] )
1864+ expect ( task . messageQueueService . isEmpty ( ) ) . toBe ( true )
1865+ } )
1866+
1867+ it ( "does not cross-drain queues between separate tasks" , async ( ) => {
1868+ const providerA = createProvider ( )
1869+ const providerB = createProvider ( )
1870+
1871+ const taskA = new Task ( {
1872+ provider : providerA ,
1873+ apiConfiguration : apiConfig ,
1874+ task : "task A" ,
1875+ startTask : false ,
1876+ } )
1877+ const taskB = new Task ( {
1878+ provider : providerB ,
1879+ apiConfiguration : apiConfig ,
1880+ task : "task B" ,
1881+ startTask : false ,
1882+ } )
1883+
1884+ vi . spyOn ( taskA as any , "getSystemPrompt" ) . mockResolvedValue ( "system" )
1885+ vi . spyOn ( taskB as any , "getSystemPrompt" ) . mockResolvedValue ( "system" )
1886+
1887+ const spyA = vi . spyOn ( taskA , "submitUserMessage" ) . mockResolvedValue ( undefined )
1888+ const spyB = vi . spyOn ( taskB , "submitUserMessage" ) . mockResolvedValue ( undefined )
1889+
1890+ taskA . messageQueueService . addMessage ( "A message" )
1891+ taskB . messageQueueService . addMessage ( "B message" )
1892+
1893+ // Condense in task A should only drain A's queue
1894+ vi . useFakeTimers ( )
1895+ await taskA . condenseContext ( )
1896+ vi . runAllTimers ( )
1897+ vi . useRealTimers ( )
1898+
1899+ expect ( spyA ) . toHaveBeenCalledWith ( "A message" , undefined )
1900+ expect ( spyB ) . not . toHaveBeenCalled ( )
1901+ expect ( taskB . messageQueueService . isEmpty ( ) ) . toBe ( false )
1902+
1903+ // Now condense in task B should drain B's queue
1904+ vi . useFakeTimers ( )
1905+ await taskB . condenseContext ( )
1906+ vi . runAllTimers ( )
1907+ vi . useRealTimers ( )
1908+
1909+ expect ( spyB ) . toHaveBeenCalledWith ( "B message" , undefined )
1910+ expect ( taskB . messageQueueService . isEmpty ( ) ) . toBe ( true )
1911+ } )
1912+ } )
0 commit comments