aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 19:44:17 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit226e814d47a5e1acd4d308619921ca1012723b88 (patch)
tree769c3a303298c5065592e81c1908adf900133073
parentce30547af39f692bef777591bdb329a7fa00e86e (diff)
fix(cdp): route background-tab events to the tab they belong to
The event loop dropped every event whose sessionId was not the active tab's, so the per-tab console and network buffers could only ever hold data captured while their tab was in the foreground. The filter now drops only sessions this instance never attached, and the dialog, frameNavigated and loadEventFired arms resolve their tab from the event's sessionId instead of assuming the active tab — without that, a background tab's dialog would have been recorded against the foreground one. Tabs are also registered before the domain-enable round trips, so events arriving during attach are no longer dropped as unknown. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f4 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--src/cdp/session.ts8
-rw-r--r--test/cdp/session-events-test.ts100
2 files changed, 104 insertions, 4 deletions
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index 39cd87e..2e0e71f 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -90,11 +90,11 @@ export const createCdpSession = (
// Filter: skip events from sessions we're not currently tracking.
// Target.targetDestroyed is browser-level (no sessionId) — always process.
if (ev.method !== "Target.targetDestroyed") {
- if (ev.sessionId && ev.sessionId !== sessionId) continue;
+ if (ev.sessionId && !sessionIdToTargetId.has(ev.sessionId)) continue;
}
switch (ev.method) {
case "Page.javascriptDialogOpening": {
- const tab = targetId ? tabs.get(targetId) : undefined;
+ const tab = resolveTab(ev.sessionId);
if (!tab) break;
const decoded = decodeEvent(ev.method, ev.params);
tab.dialog = decoded.success
@@ -114,7 +114,7 @@ export const createCdpSession = (
// was about to read. (Fix for spec §7 predictability bug #2.)
case "Page.frameNavigated":
case "Page.loadEventFired": {
- const tab = targetId ? tabs.get(targetId) : undefined;
+ const tab = resolveTab(ev.sessionId);
if (tab) tab.pageInfoDirty = true;
break;
}
@@ -333,7 +333,6 @@ export const createCdpSession = (
const attachedSessionId = attached.data;
sessionId = attachedSessionId;
targetId = pickTargetId;
- await enableDomains(attachedSessionId);
tabs.set(pickTargetId, {
sessionId: attachedSessionId,
targetId: pickTargetId,
@@ -345,6 +344,7 @@ export const createCdpSession = (
refSig: new Map(),
});
sessionIdToTargetId.set(attachedSessionId, pickTargetId);
+ await enableDomains(attachedSessionId);
return ok({ targetId: pickTargetId, sessionId: attachedSessionId });
},
async switchTo(tid) {
diff --git a/test/cdp/session-events-test.ts b/test/cdp/session-events-test.ts
new file mode 100644
index 0000000..73f849e
--- /dev/null
+++ b/test/cdp/session-events-test.ts
@@ -0,0 +1,100 @@
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
+import { setImmediate as tick } from "node:timers/promises";
+import { createCdpSession } from "../../src/cdp/session";
+import { createOwnershipRegistry } from "../../src/cdp/ownership";
+import { createStubTransport } from "../domains/fake-client";
+import { ok } from "../../src/util/result";
+
+const flush = async (): Promise<void> => {
+ await tick();
+ await tick();
+};
+
+const twoTabSession = async () => {
+ const stub = createStubTransport({
+ "Target.setDiscoverTargets": ok({}),
+ "Target.getTargets": ok({
+ targetInfos: [
+ { targetId: "t1", type: "page", title: "One", url: "https://one.test/" },
+ { targetId: "t2", type: "page", title: "Two", url: "https://two.test/" },
+ ],
+ }),
+ "Target.attachToTarget": [ok({ sessionId: "s1" }), ok({ sessionId: "s2" }), ok({ sessionId: "s3" })],
+ "Browser.getWindowForTarget": ok({ windowId: 7 }),
+ "Target.activateTarget": ok({}),
+ });
+ const ownership = createOwnershipRegistry({ ownedTargetIds: ["t1", "t2"] });
+ const session = createCdpSession(stub.transport, ownership);
+ const attached = await session.attachFirstPage();
+ assert.equal(attached.success, true);
+ await session.switchTo("t2");
+ return { stub, session };
+};
+
+describe("CdpSession event routing across tabs", () => {
+ test("a console event from a background tab reaches that tab's buffer", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Runtime.consoleAPICalled",
+ sessionId: "s1",
+ params: { type: "error", args: [{ type: "string", value: "boom" }] },
+ });
+ await flush();
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ await session.switchTo("t1");
+ const drained = session.drainConsoleBuffer({});
+ assert.equal(drained.records.length, 1);
+ assert.equal(drained.records[0]?.text, "boom");
+ });
+
+ test("a network event from a background tab reaches that tab's buffer", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Network.requestWillBeSent",
+ sessionId: "s1",
+ params: { requestId: "r1", request: { url: "https://one.test/api", method: "GET" }, type: "XHR" },
+ });
+ await flush();
+ await session.switchTo("t1");
+ const drained = session.drainNetworkBuffer({});
+ assert.equal(drained.records.length, 1);
+ assert.equal(drained.records[0]?.url, "https://one.test/api");
+ });
+
+ test("a background tab's dialog is recorded against that tab, not the active one", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Page.javascriptDialogOpening",
+ sessionId: "s1",
+ params: { type: "confirm", message: "really?" },
+ });
+ await flush();
+ assert.equal(session.takeDialog(), null);
+ await session.switchTo("t1");
+ assert.equal(session.takeDialog()?.message, "really?");
+ });
+
+ test("a background tab's navigation marks that tab's page info dirty, not the active one", async () => {
+ const { stub, session } = await twoTabSession();
+ session.drainPageInfoInvalidations();
+ stub.emit({ method: "Page.loadEventFired", sessionId: "s1", params: {} });
+ await flush();
+ assert.equal(session.drainPageInfoInvalidations(), false);
+ await session.switchTo("t1");
+ assert.equal(session.drainPageInfoInvalidations(), true);
+ });
+
+ test("an event from a session this instance never attached is ignored", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Runtime.consoleAPICalled",
+ sessionId: "unknown-session",
+ params: { type: "error", args: [{ type: "string", value: "stranger" }] },
+ });
+ await flush();
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ await session.switchTo("t1");
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ });
+});