@@ -5,7 +5,12 @@ import type { Task } from "../../../core/task/Task"
55import type { Mock } from "vitest"
66import * as vscode from "vscode"
77import delay from "delay"
8- import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
8+ import {
9+ DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES ,
10+ DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED ,
11+ DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES ,
12+ DEFAULT_WRITE_DELAY_MS ,
13+ } from "@roo-code/types"
914
1015import { makePosition , makeRange , makeTextDocument , makeTextEditor , makeUri } from "../../../test-utils/vscode"
1116
@@ -3142,4 +3147,317 @@ describe("DiffViewProvider", () => {
31423147 expect ( diffViewProvider . isEditingDifferentPath ( "dir/path.ts" ) ) . toBe ( false )
31433148 } )
31443149 } )
3150+
3151+ describe ( "blocking rows this unit owns at head d52c1614f6e4" , ( ) => {
3152+ // Each it below closes a line of the 50 blocking rows the mutation-diff leg published at
3153+ // p1066/u6-execute-error-path-cleanup d52c1614f6e4. The lines named here are the ones this
3154+ // unit's own diff defines: for each of them no earlier-merging unit in the chain
3155+ // (U3 #1931, U4 #1929, U5a #1930, U5b #2435) adds the same line against its own merge base,
3156+ // so the construct is this unit's to settle and no suppression directive is used anywhere.
3157+ const settleRelPath = "settled-target-file.ts"
3158+ const settleTargetPath = `${ mockCwd } /settled-target-file.ts`
3159+
3160+ // revertChanges() reads only uri/getText/positionAt/isDirty/save off the diff document,
3161+ // so the double stays that narrow and every input the rollback decides on is an argument.
3162+ const makeRollbackDocument = ( content : string , isDirty : boolean , saved : boolean ) => ( {
3163+ uri : makeUri ( settleTargetPath ) ,
3164+ getText : vi . fn ( ) . mockReturnValue ( content ) ,
3165+ positionAt : vi . fn ( ) . mockReturnValue ( { line : 0 , character : 0 } ) ,
3166+ isDirty,
3167+ save : vi . fn ( ) . mockResolvedValue ( saved ) ,
3168+ } )
3169+
3170+ const makeDirtyFileTab = ( ) => ( {
3171+ input : Object . assign ( new vscode . TabInputText ( makeUri ( settleTargetPath ) ) , {
3172+ uri : makeUri ( settleTargetPath ) ,
3173+ } ) ,
3174+ isDirty : true ,
3175+ label : settleRelPath ,
3176+ } )
3177+
3178+ // Both fixtures below sit on module-level VS Code properties the provider reads live, so each
3179+ // run installs its own value and hands the original descriptor back rather than leaking state.
3180+ const withTabGroupsAll = ( tabs : unknown [ ] , run : ( ) => Promise < unknown > ) => {
3181+ const original = Object . getOwnPropertyDescriptor ( vscode . window . tabGroups , "all" )
3182+ Object . defineProperty ( vscode . window . tabGroups , "all" , { get : ( ) => [ { tabs } ] , configurable : true } )
3183+ return run ( ) . finally ( ( ) => {
3184+ if ( original ) {
3185+ Object . defineProperty ( vscode . window . tabGroups , "all" , original )
3186+ }
3187+ } )
3188+ }
3189+
3190+ const withTextDocuments = ( documents : unknown [ ] , run : ( ) => Promise < unknown > ) => {
3191+ const original = Object . getOwnPropertyDescriptor ( vscode . workspace , "textDocuments" )
3192+ Object . defineProperty ( vscode . workspace , "textDocuments" , { value : documents , configurable : true } )
3193+ return run ( ) . finally ( ( ) => {
3194+ if ( original ) {
3195+ Object . defineProperty ( vscode . workspace , "textDocuments" , original )
3196+ }
3197+ } )
3198+ }
3199+
3200+ it ( "line 596: reports no different path when the session flag is up but its retained path is gone" , ( ) => {
3201+ // The guard's other exit. isEditing can stay true with relPath already cleared by a reset
3202+ // whose tab close rejected, and falling through to the comparison would then call every
3203+ // later write a second write, because undefined differs from every path.
3204+ Object . assign ( diffViewProvider , { isEditing : true , relPath : undefined } )
3205+
3206+ expect ( diffViewProvider . isEditingDifferentPath ( "any/path.ts" ) ) . toBe ( false )
3207+ } )
3208+
3209+ it ( "line 833: restores the recorded content rather than an empty buffer when a new-file rollback meets a dirty diff buffer" , async ( ) => {
3210+ const document = makeRollbackDocument ( "partial model output" , true , true )
3211+ Object . assign ( diffViewProvider , {
3212+ relPath : settleRelPath ,
3213+ activeDiffEditor : { document } ,
3214+ editType : "create" ,
3215+ createdDirs : [ ] ,
3216+ placeholderPath : undefined ,
3217+ originalContent : "the content the file held before this write" ,
3218+ } )
3219+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3220+
3221+ await withTabGroupsAll ( [ makeDirtyFileTab ( ) ] , ( ) => diffViewProvider . revertChanges ( ) )
3222+
3223+ const [ , , replacedText ] = mockWorkspaceEdit . replace . mock . calls [ 0 ]
3224+ expect ( replacedText ) . toBe ( "the content the file held before this write" )
3225+ } )
3226+
3227+ it ( "line 833: falls back to an empty buffer text, not a placeholder string, when a new-file rollback has no recorded original" , async ( ) => {
3228+ const document = makeRollbackDocument ( "partial model output" , true , true )
3229+ Object . assign ( diffViewProvider , {
3230+ relPath : settleRelPath ,
3231+ activeDiffEditor : { document } ,
3232+ editType : "create" ,
3233+ createdDirs : [ ] ,
3234+ placeholderPath : undefined ,
3235+ originalContent : undefined ,
3236+ } )
3237+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3238+
3239+ await withTabGroupsAll ( [ makeDirtyFileTab ( ) ] , ( ) => diffViewProvider . revertChanges ( ) )
3240+
3241+ const [ , , replacedText ] = mockWorkspaceEdit . replace . mock . calls [ 0 ]
3242+ expect ( replacedText ) . toBe ( "" )
3243+ } )
3244+
3245+ it ( "line 841: reports the unconfirmed save of the diff buffer it just restored, naming the path and the stopped work" , async ( ) => {
3246+ // applyEdit confirmed, document.save() did not: the rollback stops before any close or
3247+ // unlink, and the report is the only thing the caller gets, so it has to carry the reason.
3248+ const document = makeRollbackDocument ( "partial model output" , true , false )
3249+ Object . assign ( diffViewProvider , {
3250+ relPath : settleRelPath ,
3251+ activeDiffEditor : { document } ,
3252+ editType : "create" ,
3253+ createdDirs : [ ] ,
3254+ placeholderPath : settleTargetPath ,
3255+ originalContent : "" ,
3256+ } )
3257+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3258+
3259+ await withTabGroupsAll ( [ makeDirtyFileTab ( ) ] , async ( ) => {
3260+ await expect ( diffViewProvider . revertChanges ( ) ) . rejects . toThrowError (
3261+ new Error (
3262+ `Rollback could not save the restored buffer for ${ settleTargetPath } ; its clean state is unconfirmed, so the rollback stopped before closing tabs or deleting the file.` ,
3263+ ) ,
3264+ )
3265+ } )
3266+
3267+ expect ( vscode . window . tabGroups . close ) . not . toHaveBeenCalled ( )
3268+ expect ( fs . unlink ) . not . toHaveBeenCalled ( )
3269+ } )
3270+
3271+ it ( "line 883: restores the recorded content rather than an empty buffer when an existing file is rolled back" , async ( ) => {
3272+ const document = makeRollbackDocument ( "streamed content" , false , true )
3273+ Object . assign ( diffViewProvider , {
3274+ relPath : settleRelPath ,
3275+ activeDiffEditor : { document } ,
3276+ editType : "modify" ,
3277+ createdDirs : [ ] ,
3278+ originalContent : "the content the file held before this write" ,
3279+ } )
3280+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3281+ diffViewProvider [ "closeAllDiffViews" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3282+ diffViewProvider [ "keepOrCloseEditedFile" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3283+
3284+ await diffViewProvider . revertChanges ( )
3285+
3286+ const [ , , replacedText ] = mockWorkspaceEdit . replace . mock . calls [ 0 ]
3287+ expect ( replacedText ) . toBe ( "the content the file held before this write" )
3288+ } )
3289+
3290+ it ( "line 883: strips an empty buffer text rather than a stand-in when a modify rollback has no recorded original" , async ( ) => {
3291+ const document = makeRollbackDocument ( "streamed content" , false , true )
3292+ Object . assign ( diffViewProvider , {
3293+ relPath : settleRelPath ,
3294+ activeDiffEditor : { document } ,
3295+ editType : "modify" ,
3296+ createdDirs : [ ] ,
3297+ originalContent : undefined ,
3298+ } )
3299+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3300+ diffViewProvider [ "closeAllDiffViews" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3301+ diffViewProvider [ "keepOrCloseEditedFile" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3302+
3303+ await diffViewProvider . revertChanges ( )
3304+
3305+ const [ , , replacedText ] = mockWorkspaceEdit . replace . mock . calls [ 0 ]
3306+ expect ( replacedText ) . toBe ( "" )
3307+ } )
3308+
3309+ it ( "lines 896 and 901 and 902 and 904: rolls back to the centralized defaults when the task behind the rollback is gone" , async ( ) => {
3310+ // revertChanges() reads its auto-close settings through taskRef -> providerRef -> getState().
3311+ // Any link of that chain can already be gone by the time a denial lands, and a rollback that
3312+ // throws while reading them would take the session teardown with it. It must instead fall
3313+ // back to the same defaults saveChanges() uses.
3314+ const document = makeRollbackDocument ( "streamed content" , false , true )
3315+ const keepOrClose = vi . fn ( ) . mockResolvedValue ( undefined )
3316+ Object . assign ( diffViewProvider , {
3317+ relPath : settleRelPath ,
3318+ activeDiffEditor : { document } ,
3319+ editType : "modify" ,
3320+ createdDirs : [ ] ,
3321+ originalContent : "original" ,
3322+ } )
3323+ diffViewProvider [ "taskRef" ] = { deref : ( ) => undefined } as unknown as WeakRef < Task >
3324+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3325+ diffViewProvider [ "closeAllDiffViews" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3326+ diffViewProvider [ "keepOrCloseEditedFile" ] = keepOrClose
3327+
3328+ await diffViewProvider . revertChanges ( )
3329+
3330+ const args = keepOrClose . mock . calls [ 0 ]
3331+ expect ( args [ 0 ] ) . toBe ( settleTargetPath )
3332+ expect ( args [ 1 ] ) . toBe ( false )
3333+ expect ( args [ 2 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES )
3334+ expect ( args [ 3 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED )
3335+ expect ( args [ 4 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES )
3336+ } )
3337+
3338+ it ( "line 896: rolls back to the centralized defaults when the task no longer holds a provider" , async ( ) => {
3339+ const document = makeRollbackDocument ( "streamed content" , false , true )
3340+ const keepOrClose = vi . fn ( ) . mockResolvedValue ( undefined )
3341+ Object . assign ( diffViewProvider , {
3342+ relPath : settleRelPath ,
3343+ activeDiffEditor : { document } ,
3344+ editType : "modify" ,
3345+ createdDirs : [ ] ,
3346+ originalContent : "original" ,
3347+ } )
3348+ diffViewProvider [ "taskRef" ] = {
3349+ deref : ( ) => ( { providerRef : { deref : ( ) => undefined } } ) ,
3350+ } as unknown as WeakRef < Task >
3351+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3352+ diffViewProvider [ "closeAllDiffViews" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3353+ diffViewProvider [ "keepOrCloseEditedFile" ] = keepOrClose
3354+
3355+ await diffViewProvider . revertChanges ( )
3356+
3357+ const args = keepOrClose . mock . calls [ 0 ]
3358+ expect ( args [ 2 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES )
3359+ expect ( args [ 3 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED )
3360+ expect ( args [ 4 ] ) . toBe ( DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES )
3361+ } )
3362+
3363+ it ( "lines 902 and 904: carries the settings the state holds through the rollback path" , async ( ) => {
3364+ // The rollback path is the one the auto-close decision table never exercised: every row of
3365+ // that table drives saveChanges(). Opting in on the rollback path has to close the tab, or
3366+ // the settings would mean one thing on accept and another on deny.
3367+ const document = makeRollbackDocument ( "streamed content" , false , true )
3368+ const closeFileTab = vi . fn ( ) . mockResolvedValue ( undefined )
3369+ const showEditedFile = vi . fn ( ) . mockResolvedValue ( undefined )
3370+ Object . assign ( diffViewProvider , {
3371+ relPath : settleRelPath ,
3372+ activeDiffEditor : { document } ,
3373+ editType : "modify" ,
3374+ createdDirs : [ ] ,
3375+ originalContent : "original" ,
3376+ documentWasOpen : false ,
3377+ userTouchedDocument : false ,
3378+ userTouchedDiffEditor : false ,
3379+ } )
3380+ diffViewProvider [ "taskRef" ] = {
3381+ deref : ( ) =>
3382+ makeTaskDouble ( {
3383+ autoCloseZooOpenedFiles : true ,
3384+ autoCloseZooOpenedFilesAfterUserEdited : true ,
3385+ autoCloseZooOpenedNewFiles : true ,
3386+ } ) ,
3387+ } as unknown as WeakRef < Task >
3388+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValue ( true )
3389+ diffViewProvider [ "closeAllDiffViews" ] = vi . fn ( ) . mockResolvedValue ( undefined )
3390+ diffViewProvider [ "closeFileTab" ] = closeFileTab
3391+ diffViewProvider [ "showEditedFileWithoutDisruptingFocus" ] = showEditedFile
3392+
3393+ await diffViewProvider . revertChanges ( )
3394+
3395+ expect ( closeFileTab ) . toHaveBeenCalledWith ( settleTargetPath )
3396+ expect ( showEditedFile ) . not . toHaveBeenCalled ( )
3397+ } )
3398+
3399+ it ( "lines 1209 and 1211: stops the rollback and reports the refused restore of the file-scheme buffer by name" , async ( ) => {
3400+ // The diff-side restore confirms; the plain file-scheme buffer holding the same unapproved
3401+ // content is the one VS Code refuses to put back. Closing it anyway would hand the user a
3402+ // save prompt over exactly the content this rollback discards.
3403+ const document = makeRollbackDocument ( "partial model output" , true , true )
3404+ const streamedBuffer = {
3405+ uri : makeUri ( settleTargetPath ) ,
3406+ scheme : "file" ,
3407+ getText : vi . fn ( ) . mockReturnValue ( "partial model output" ) ,
3408+ positionAt : vi . fn ( ) . mockReturnValue ( { line : 0 , character : 0 } ) ,
3409+ isDirty : true ,
3410+ save : vi . fn ( ) . mockResolvedValue ( true ) ,
3411+ }
3412+ Object . assign ( diffViewProvider , {
3413+ relPath : settleRelPath ,
3414+ activeDiffEditor : { document } ,
3415+ editType : "create" ,
3416+ createdDirs : [ ] ,
3417+ placeholderPath : settleTargetPath ,
3418+ originalContent : "" ,
3419+ } )
3420+ vi . mocked ( vscode . workspace . applyEdit ) . mockResolvedValueOnce ( true ) . mockResolvedValueOnce ( false )
3421+
3422+ await withTextDocuments ( [ streamedBuffer ] , ( ) =>
3423+ withTabGroupsAll ( [ makeDirtyFileTab ( ) ] , async ( ) => {
3424+ await expect ( diffViewProvider . revertChanges ( ) ) . rejects . toThrowError (
3425+ new Error (
3426+ `Rollback could not restore the buffer for ${ settleTargetPath } ; it may still hold unapproved content, so the tab was left open rather than closed.` ,
3427+ ) ,
3428+ )
3429+ } ) ,
3430+ )
3431+
3432+ expect ( streamedBuffer . save ) . not . toHaveBeenCalled ( )
3433+ expect ( vscode . window . tabGroups . close ) . not . toHaveBeenCalled ( )
3434+ expect ( fs . unlink ) . not . toHaveBeenCalled ( )
3435+ } )
3436+
3437+ it ( "lines 1558 and 1559 and 1563 and 1564 and 1569 and 1570 and 1571: clears every session field the reset owns, not only the ones with a visible effect" , async ( ) => {
3438+ // A flag that survives reset() is read by the NEXT edit as if it described that edit:
3439+ // documentWasOpen decides whether the tab is restored instead of closed, the two touched
3440+ // flags decide auto-close, and streamedLines/preDiagnostics/snapshotPreviewTabs carry one
3441+ // edit's stream, diagnostics and tab snapshot into the next one.
3442+ Object . assign ( diffViewProvider , {
3443+ documentWasOpen : true ,
3444+ documentWasPinned : true ,
3445+ streamedLines : [ "leftover streamed line" ] ,
3446+ preDiagnostics : [ { message : "leftover diagnostic" } ] ,
3447+ userTouchedDocument : true ,
3448+ userTouchedDiffEditor : true ,
3449+ snapshotPreviewTabs : [ makePreviewTab ( `${ mockCwd } /leftover-preview.ts` ) ] ,
3450+ } )
3451+
3452+ await diffViewProvider . reset ( )
3453+
3454+ expect ( diffViewProvider [ "documentWasOpen" ] ) . toBe ( false )
3455+ expect ( diffViewProvider [ "documentWasPinned" ] ) . toBe ( false )
3456+ expect ( diffViewProvider [ "userTouchedDocument" ] ) . toBe ( false )
3457+ expect ( diffViewProvider [ "userTouchedDiffEditor" ] ) . toBe ( false )
3458+ expect ( diffViewProvider [ "streamedLines" ] ) . toEqual ( [ ] )
3459+ expect ( diffViewProvider [ "preDiagnostics" ] ) . toEqual ( [ ] )
3460+ expect ( diffViewProvider [ "snapshotPreviewTabs" ] ) . toEqual ( [ ] )
3461+ } )
3462+ } )
31453463} )
0 commit comments