@@ -1861,6 +1861,113 @@ describe("DiffViewProvider", () => {
18611861 expect ( fs . unlink ) . toHaveBeenCalledWith ( expect . stringContaining ( "mock-target-file.ts" ) )
18621862 } )
18631863
1864+ it ( "keeps an editor that arrives after the deferred diff command from publishing into a released session" , async ( ) => {
1865+ // The reviewer named this ordering: a release can reach the provider while open() is still
1866+ // parked inside openDiffEditor(), before the vscode.diff command settles. This row drives the
1867+ // real open() against a mocked executeCommand("vscode.diff") that resolves only when the
1868+ // test releases it, and leaves openDiffEditor() itself alone because it is private. What it
1869+ // pins is open() reacting to a session that was released before that command settled, on the
1870+ // create path where open() wrote an empty placeholder - not the extension host that would
1871+ // really raise these events.
1872+ const callOrder : string [ ] = [ ]
1873+ const lateDocument = makeTextDocument ( {
1874+ uri : makeUri ( `${ mockCwd } /test.txt` ) ,
1875+ isDirty : true ,
1876+ getText : ( ) => "partial model output" ,
1877+ } )
1878+ const lateEditor = makeTextEditor ( { document : lateDocument } )
1879+ let resolveDiffCommand : ( ) => void = ( ) => { }
1880+ const diffCommandSettled = new Promise < void > ( ( resolve ) => {
1881+ resolveDiffCommand = resolve
1882+ } )
1883+ // openDiffEditor() registers these listeners before it invokes the diff command, so the
1884+ // doubles hand the callbacks back instead of being stubbed away.
1885+ let onDidOpenTextDocument : ( ( document : vscode . TextDocument ) => unknown ) | undefined
1886+ vi . mocked ( vscode . workspace . onDidOpenTextDocument ) . mockImplementation ( ( callback ) => {
1887+ onDidOpenTextDocument = callback
1888+ return { dispose : vi . fn ( ) }
1889+ } )
1890+ // The other three listeners stay on the module mock's own disposable double, so
1891+ // registering them is observable without this row replacing anything.
1892+ Object . defineProperty ( vscode . window . tabGroups , "all" , { value : [ ] , configurable : true } )
1893+ vi . mocked ( vscode . window ) . visibleTextEditors = [ ]
1894+ vi . mocked ( vscode . window . showTextDocument ) . mockResolvedValue ( lateEditor )
1895+ vi . mocked ( vscode . commands . executeCommand ) . mockImplementation ( async ( command ) => {
1896+ callOrder . push ( String ( command ) )
1897+ // Only this test lets the diff command settle, which is what defers the diff editor.
1898+ await diffCommandSettled
1899+ callOrder . push ( "diffCommandSettled" )
1900+ } )
1901+ // A denied write_to_file empties the buffer; the replace is what would be saved.
1902+ vi . mocked ( vscode . workspace . applyEdit ) . mockImplementation ( async ( ) => {
1903+ callOrder . push ( "applyEdit" )
1904+ return true
1905+ } )
1906+ Object . assign ( diffViewProvider , {
1907+ editType : "create" as const ,
1908+ // The two tab helpers are recorded so the order below names the closings rather than
1909+ // re-testing what each of them does.
1910+ closeAllDiffViews : vi . fn ( async ( ) => {
1911+ callOrder . push ( "closeDiffViews" )
1912+ } ) ,
1913+ closeFileTab : vi . fn ( async ( ) => {
1914+ callOrder . push ( "closeFileTab" )
1915+ } ) ,
1916+ } )
1917+ const errorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
1918+ vi . mocked ( fs . readFile ) . mockResolvedValue ( "content the user saved after open()" )
1919+ vi . mocked ( fs . unlink ) . mockResolvedValue ( undefined )
1920+ const openPromise = diffViewProvider . open ( "test.txt" )
1921+ // The streamed text is what the deny discards; open() owns the placeholder it wrote.
1922+ diffViewProvider [ "streamedLines" ] = [ "partial model output" ]
1923+ // Let open() run up to the point where it invokes the diff command and stop it there:
1924+ // this macrotask queues behind every already-pending microtask, and the command
1925+ // double stays unresolved, so open() is parked exactly where the reviewer named.
1926+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
1927+ // open() publishes its editor only when the await below it resolves, so the field
1928+ // starts empty for this scenario rather than carrying the double the beforeEach installs.
1929+ diffViewProvider [ "activeDiffEditor" ] = undefined
1930+ // open() is parked on the diff command, so it has published no editor yet.
1931+ expect ( callOrder ) . toEqual ( [ "vscode.diff" ] )
1932+ expect ( diffViewProvider [ "activeDiffEditor" ] ) . toBeUndefined ( )
1933+ expect ( vscode . commands . executeCommand ) . toHaveBeenCalledWith (
1934+ "vscode.diff" ,
1935+ expect . anything ( ) ,
1936+ expect . anything ( ) ,
1937+ expect . anything ( ) ,
1938+ { preserveFocus : true } ,
1939+ )
1940+ // The release runs while open() is still awaiting that command.
1941+ const released = diffViewProvider . discardUnapprovedStream ( )
1942+ await expect ( released ) . rejects . toThrow ( / n o l o n g e r h o l d s t h e e m p t y p l a c e h o l d e r / )
1943+ // The release refused before any closing: with no editor published it has nothing to work
1944+ // from, so it neither empties the buffer nor removes the placeholder it no longer owns.
1945+ expect ( callOrder ) . toEqual ( [ "vscode.diff" ] )
1946+ expect ( vscode . workspace . applyEdit ) . not . toHaveBeenCalled ( )
1947+ expect ( lateDocument . save ) . not . toHaveBeenCalled ( )
1948+ // Then the host catches up: the command settles and the editor finally appears.
1949+ resolveDiffCommand ( )
1950+ vi . mocked ( vscode . window ) . visibleTextEditors = [ lateEditor ]
1951+ if ( typeof onDidOpenTextDocument !== "function" ) {
1952+ throw new Error ( "open() never registered the document listener it uses to await the diff editor" )
1953+ }
1954+ await onDidOpenTextDocument ( lateDocument )
1955+ await openPromise
1956+ // The late editor never published into the released session.
1957+ expect ( diffViewProvider [ "activeDiffEditor" ] ) . toBeUndefined ( )
1958+ expect ( DecorationController ) . not . toHaveBeenCalled ( )
1959+ expect ( vscode . window . onDidChangeActiveTextEditor ) . not . toHaveBeenCalled ( )
1960+ expect ( vscode . window . onDidChangeTextEditorSelection ) . not . toHaveBeenCalled ( )
1961+ expect ( vscode . window . onDidChangeTextEditorVisibleRanges ) . not . toHaveBeenCalled ( )
1962+ expect ( fs . unlink ) . not . toHaveBeenCalled ( )
1963+ // The only closing after the release is the one open() performs on the late editor through
1964+ // the same helper the release paths use; the diff command settling is recorded first.
1965+ expect ( callOrder ) . toEqual ( [ "vscode.diff" , "diffCommandSettled" , "closeDiffViews" ] )
1966+ // Leave the document-open double as the module mock defined it, for the rows after
1967+ // this one, since a mockImplementation here outlives the test that set it.
1968+ vi . mocked ( vscode . workspace . onDidOpenTextDocument ) . mockImplementation ( ( ) => ( { dispose : vi . fn ( ) } ) )
1969+ errorSpy . mockRestore ( )
1970+ } )
18641971 it ( "does not save the emptied buffer over content that arrived in the placeholder after open()" , async ( ) => {
18651972 // The reviewer's ordering ask, at the layer that would have caught it. The tests above all
18661973 // run with a dirty buffer and no recorded placeholder content, so open() wrote nothing this
0 commit comments