Skip to content

Commit f65ab53

Browse files
test(core): pin the release against a diff command that resolves late
Adds the row the review asked for: a release that lands while open() is still waiting on the deferred vscode.diff command, driven through the real open() with a command double that resolves only when the test releases it. The row tests that ordering on the create path and does not stand in for the extension host.
1 parent 054af49 commit f65ab53

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

‎src/integrations/editor/__tests__/DiffViewProvider.spec.ts‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/no longer holds the empty placeholder/)
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

Comments
 (0)