11import { describe , it , expect , vi , beforeEach } from "vitest"
2+ import pWaitFor from "p-wait-for"
23import { webviewMessageHandler } from "../webviewMessageHandler"
34import { saveTaskMessages } from "../../task-persistence"
45import { handleCheckpointRestoreOperation } from "../checkpointRestoreHandler"
@@ -7,6 +8,13 @@ import { MessageManager } from "../../message-manager"
78// Mock dependencies
89vi . mock ( "../../task-persistence" )
910vi . mock ( "../checkpointRestoreHandler" )
11+ vi . mock ( "p-wait-for" , ( ) => ( {
12+ default : vi . fn ( async ( condition : ( ) => boolean ) => {
13+ if ( ! condition ( ) ) {
14+ throw new Error ( "condition not met" )
15+ }
16+ } ) ,
17+ } ) )
1018vi . mock ( "vscode" , ( ) => ( {
1119 window : {
1220 showErrorMessage : vi . fn ( ) ,
@@ -26,6 +34,7 @@ describe("webviewMessageHandler - checkpoint operations", () => {
2634 // Setup mock Cline instance
2735 mockCline = {
2836 taskId : "test-task-123" ,
37+ isInitialized : true ,
2938 clineMessages : [
3039 { ts : 1 , type : "user" , say : "user" , text : "First message" } ,
3140 { ts : 2 , type : "assistant" , say : "checkpoint_saved" , text : "abc123" } ,
@@ -37,6 +46,7 @@ describe("webviewMessageHandler - checkpoint operations", () => {
3746 { ts : 3 , role : "user" , content : [ { type : "text" , text : "Message to delete" } ] } ,
3847 { ts : 4 , role : "assistant" , content : [ { type : "text" , text : "After message" } ] } ,
3948 ] ,
49+ checkpointDiff : vi . fn ( ) ,
4050 checkpointRestore : vi . fn ( ) ,
4151 overwriteClineMessages : vi . fn ( ) ,
4252 overwriteApiConversationHistory : vi . fn ( ) ,
@@ -52,6 +62,7 @@ describe("webviewMessageHandler - checkpoint operations", () => {
5262 } ) ) ,
5363 createTaskWithHistoryItem : vi . fn ( ) ,
5464 setPendingEditOperation : vi . fn ( ) ,
65+ cancelTask : vi . fn ( ) ,
5566 contextProxy : {
5667 globalStorageUri : { fsPath : "/test/storage" } ,
5768 } ,
@@ -134,4 +145,109 @@ describe("webviewMessageHandler - checkpoint operations", () => {
134145 } )
135146 } )
136147 } )
148+
149+ describe ( "completion checkpoint actions" , ( ) => {
150+ beforeEach ( ( ) => {
151+ mockCline . clineMessages = [
152+ { ts : 1 , type : "say" , say : "text" , text : "Initial task" } ,
153+ { ts : 2 , type : "say" , say : "checkpoint_saved" , text : "initial-checkpoint" } ,
154+ { ts : 3 , type : "say" , say : "user_feedback" , text : "Latest prompt" } ,
155+ { ts : 4 , type : "say" , say : "checkpoint_saved" , text : "latest-prompt-checkpoint" } ,
156+ { ts : 5 , type : "say" , say : "completion_result" , text : "Task complete" } ,
157+ { ts : 6 , type : "ask" , ask : "completion_result" , text : "" , partial : false } ,
158+ ]
159+ } )
160+
161+ it ( "diffs changes from the checkpoint created after the latest prompt" , async ( ) => {
162+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointDiff" } )
163+
164+ expect ( mockCline . checkpointDiff ) . toHaveBeenCalledWith ( {
165+ ts : 4 ,
166+ commitHash : "latest-prompt-checkpoint" ,
167+ mode : "to-current" ,
168+ } )
169+ } )
170+
171+ it ( "restores files and task state to the checkpoint created after the latest prompt" , async ( ) => {
172+ const callOrder : string [ ] = [ ]
173+ mockProvider . cancelTask . mockImplementation ( async ( ) => callOrder . push ( "cancelTask" ) )
174+ mockCline . checkpointRestore . mockImplementation ( async ( ) => callOrder . push ( "checkpointRestore" ) )
175+
176+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointRestore" } )
177+
178+ expect ( mockProvider . cancelTask ) . toHaveBeenCalled ( )
179+ expect ( mockCline . checkpointRestore ) . toHaveBeenCalledWith ( {
180+ ts : 4 ,
181+ commitHash : "latest-prompt-checkpoint" ,
182+ mode : "restore" ,
183+ } )
184+ expect ( callOrder ) . toEqual ( [ "cancelTask" , "checkpointRestore" ] )
185+ } )
186+
187+ it ( "does not diff or restore when no latest-prompt checkpoint exists" , async ( ) => {
188+ mockCline . clineMessages = [
189+ { ts : 1 , type : "say" , say : "text" , text : "Initial task" } ,
190+ { ts : 2 , type : "say" , say : "user_feedback" , text : "Latest prompt" } ,
191+ { ts : 3 , type : "ask" , ask : "completion_result" , text : "" , partial : false } ,
192+ ]
193+
194+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointDiff" } )
195+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointRestore" } )
196+
197+ expect ( mockCline . checkpointDiff ) . not . toHaveBeenCalled ( )
198+ expect ( mockCline . checkpointRestore ) . not . toHaveBeenCalled ( )
199+ expect ( mockProvider . cancelTask ) . not . toHaveBeenCalled ( )
200+ } )
201+
202+ it ( "resolves the latest completion checkpoint in the extension host" , async ( ) => {
203+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointDiff" } )
204+
205+ expect ( mockCline . checkpointDiff ) . toHaveBeenCalledWith ( {
206+ ts : 4 ,
207+ commitHash : "latest-prompt-checkpoint" ,
208+ mode : "to-current" ,
209+ } )
210+ } )
211+
212+ it ( "does not restore when task re-initialization times out" , async ( ) => {
213+ ; ( pWaitFor as any ) . mockRejectedValueOnce ( new Error ( "timed out" ) )
214+
215+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointRestore" } )
216+
217+ expect ( mockProvider . cancelTask ) . toHaveBeenCalled ( )
218+ expect ( mockCline . checkpointRestore ) . not . toHaveBeenCalled ( )
219+ const vscode = await import ( "vscode" )
220+ expect ( vscode . window . showErrorMessage ) . toHaveBeenCalledWith ( "errors.checkpoint_timeout" )
221+ } )
222+
223+ it ( "shows an error when completion checkpoint restore fails" , async ( ) => {
224+ const restoreError = new Error ( "restore failed" )
225+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
226+ mockCline . checkpointRestore . mockRejectedValueOnce ( restoreError )
227+
228+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointRestore" } )
229+
230+ const vscode = await import ( "vscode" )
231+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
232+ "[completionCheckpointRestore] checkpointRestore failed:" ,
233+ restoreError ,
234+ )
235+ expect ( vscode . window . showErrorMessage ) . toHaveBeenCalledWith ( "errors.checkpoint_failed" )
236+ consoleErrorSpy . mockRestore ( )
237+ } )
238+
239+ it ( "does not restore when task identity changes during cancellation" , async ( ) => {
240+ mockProvider . getCurrentTask . mockReturnValueOnce ( mockCline ) . mockReturnValue ( {
241+ ...mockCline ,
242+ taskId : "different-task-id" ,
243+ } )
244+
245+ await webviewMessageHandler ( mockProvider , { type : "completionCheckpointRestore" } )
246+
247+ expect ( mockProvider . cancelTask ) . toHaveBeenCalled ( )
248+ expect ( mockCline . checkpointRestore ) . not . toHaveBeenCalled ( )
249+ const vscode = await import ( "vscode" )
250+ expect ( vscode . window . showErrorMessage ) . toHaveBeenCalledWith ( "errors.checkpoint_failed" )
251+ } )
252+ } )
137253} )
0 commit comments