mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix(reliability): prevent webview postMessage crashes and make dispose idempotent (#11313)
* fix(reliability): prevent webview postMessage crashes and make dispose idempotent Closes: #11311 1. postMessageToWebview() now catches rejections from webview.postMessage() so that messages sent after the webview is disposed do not surface as unhandled promise rejections. 2. dispose() is guarded by a _disposed flag so that repeated calls (e.g. during rapid extension deactivation) are no-ops. 3. CloudService mock in ClineProvider.spec.ts updated to include off() — a pre-existing gap exposed by the new dispose test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add early _disposed check in postMessageToWebview Skip the postMessage call entirely when the provider is already disposed, avoiding unnecessary try/catch execution. Added test coverage for this path. * chore: trigger CI --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
parent
115d6c5fce
commit
62a0106ce0
2 changed files with 53 additions and 1 deletions
|
|
@ -148,6 +148,7 @@ export class ClineProvider
|
|||
private taskCreationCallback: (task: Task) => void
|
||||
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
|
||||
private currentWorkspacePath: string | undefined
|
||||
private _disposed = false
|
||||
|
||||
private recentTasksCache?: string[]
|
||||
private taskHistoryWriteLock: Promise<void> = Promise.resolve()
|
||||
|
|
@ -579,6 +580,11 @@ export class ClineProvider
|
|||
}
|
||||
|
||||
async dispose() {
|
||||
if (this._disposed) {
|
||||
return
|
||||
}
|
||||
|
||||
this._disposed = true
|
||||
this.log("Disposing ClineProvider...")
|
||||
|
||||
// Clear all tasks from the stack.
|
||||
|
|
@ -1078,7 +1084,15 @@ export class ClineProvider
|
|||
}
|
||||
|
||||
public async postMessageToWebview(message: ExtensionMessage) {
|
||||
await this.view?.webview.postMessage(message)
|
||||
if (this._disposed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.view?.webview.postMessage(message)
|
||||
} catch {
|
||||
// View disposed, drop message silently
|
||||
}
|
||||
}
|
||||
|
||||
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ vi.mock("@roo-code/cloud", () => ({
|
|||
get instance() {
|
||||
return {
|
||||
isAuthenticated: vi.fn().mockReturnValue(false),
|
||||
off: vi.fn(),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -602,6 +603,43 @@ describe("ClineProvider", () => {
|
|||
expect(mockPostMessage).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
test("postMessageToWebview does not throw when webview is disposed", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
// Simulate postMessage throwing after webview disposal
|
||||
mockPostMessage.mockRejectedValueOnce(new Error("Webview is disposed"))
|
||||
|
||||
const message: ExtensionMessage = { type: "action", action: "chatButtonClicked" }
|
||||
|
||||
// Should not throw
|
||||
await expect(provider.postMessageToWebview(message)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
test("postMessageToWebview skips postMessage after dispose", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
await provider.dispose()
|
||||
mockPostMessage.mockClear()
|
||||
|
||||
const message: ExtensionMessage = { type: "action", action: "chatButtonClicked" }
|
||||
await provider.postMessageToWebview(message)
|
||||
|
||||
expect(mockPostMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("dispose is idempotent — second call is a no-op", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
await provider.dispose()
|
||||
await provider.dispose()
|
||||
|
||||
// dispose body runs only once: log "Disposing ClineProvider..." appears once
|
||||
const disposeCalls = (mockOutputChannel.appendLine as ReturnType<typeof vi.fn>).mock.calls.filter(
|
||||
([msg]) => typeof msg === "string" && msg.includes("Disposing ClineProvider..."),
|
||||
)
|
||||
expect(disposeCalls).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("handles webviewDidLaunch message", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue