Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/extension/src/browser-driver/__tests__/chromium-cdp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ describe("ChromiumCdp", () => {
expect(cdp.isAttached(5)).toBe(false);
});

it("rolls back the raw attach when a domain enable fails so the tab is not left stuck", async () => {
const { api } = fakeApi();
(api.sendCommand as ReturnType<typeof vi.fn>).mockImplementation(
async (_target, method: string) => {
if (method === "Page.enable") throw new Error("Page.enable rejected");
return {};
},
);
const cdp = new ChromiumCdp(api);

// Raw attach succeeds, but Page.enable fails. Without the rollback
// the debugger would stay attached while `attachedTabs` omits the id,
// leaving the tab stuck on "Another debugger is already attached" for
// every later call until the extension is reloaded.
await expect(cdp.ensureAttached(42)).rejects.toThrow(/Page\.enable rejected/);
expect(cdp.isAttached(42)).toBe(false);
expect(api.detach).toHaveBeenCalledWith({ tabId: 42 });

// The rollback detached, so a fresh attach can succeed afterwards.
(api.sendCommand as ReturnType<typeof vi.fn>).mockResolvedValue({});
await expect(cdp.ensureAttached(42)).resolves.toBeUndefined();
expect(cdp.isAttached(42)).toBe(true);
});

it("send() rejects on chrome.runtime.lastError-style failures", async () => {
const { api } = fakeApi();
(api.sendCommand as ReturnType<typeof vi.fn>).mockImplementation(
Expand Down
24 changes: 20 additions & 4 deletions apps/extension/src/browser-driver/chromium-cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,26 @@ export class ChromiumCdp {
}
const attach = (async () => {
await this.api.attach({ tabId }, CDP_PROTOCOL_VERSION);
await this.enablePageDomain(tabId);
await this.enableConsoleDomains(tabId);
await this.enableNetworkDomainBestEffort(tabId);
this.attachedTabs.add(tabId);
try {
await this.enablePageDomain(tabId);
await this.enableConsoleDomains(tabId);
await this.enableNetworkDomainBestEffort(tabId);
this.attachedTabs.add(tabId);
} catch (err) {
// A CDP domain enable failed after the raw attach succeeded
// (e.g. `Page.enable` rejects because the tab just navigated to
// a chrome:// URL or the Web Store). `attachedTabs` does not yet
// hold `tabId`, so without rolling back the debugger would stay
// attached and the next `ensureAttached` would re-attach and hit
// "Another debugger is already attached" forever — the tab is
// unusable until the extension is reloaded. Detach directly
// (`this.detach()` is a no-op here because `attachedTabs` lacks
// the id) so the next attempt starts from a clean slate.
await this.api.detach({ tabId }).catch((detachErr) => {
console.debug("[bsk cdp] rollback detach failed", { tabId, detachErr });
});
throw err;
}
})()
.catch((err) => {
// Chrome surfaces "Another debugger is already attached" when
Expand Down