From 041ec4ad10907cea2d4d161caa5aaf1ba6669aa7 Mon Sep 17 00:00:00 2001 From: John Wooten <60260750+JFWooten4@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:54:20 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9E=20Handle=20disconnected=20PDF?= =?UTF-8?q?=20summary=20relay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consume Chrome's connection error in the PDF summary port's disconnect handler, release its timer, and reject pending acknowledgements without leaving a stale resolver. Co-authored-by: Codex Web --- src/viewer/sharing/summarize-with-chatgpt.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/viewer/sharing/summarize-with-chatgpt.js b/src/viewer/sharing/summarize-with-chatgpt.js index 51bd00e..55d3b95 100644 --- a/src/viewer/sharing/summarize-with-chatgpt.js +++ b/src/viewer/sharing/summarize-with-chatgpt.js @@ -71,8 +71,15 @@ function registerPdfTransfer(requestId, filename) { let awaitingAck; const expire = setTimeout(() => port.disconnect(), 120000); port.onDisconnect.addListener(() => { + // Chrome reports a missing receiver through lastError on disconnect. + // Reading it here also prevents an "Unchecked runtime.lastError" warning. + const disconnectError = chrome.runtime.lastError?.message; stopped = true; - if (awaitingAck) awaitingAck.reject(new Error("PDF transfer disconnected")); + clearTimeout(expire); + if (awaitingAck) { + awaitingAck.reject(new Error(disconnectError || "PDF transfer disconnected")); + awaitingAck = null; + } }); port.onMessage.addListener((message) => { if (message?.type === "ack" && awaitingAck) { From 28928f211afa65dd9d4b438f5ebdc7e3fbe17917 Mon Sep 17 00:00:00 2001 From: John Wooten <60260750+JFWooten4@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:54:37 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9E=20Test=20missing=20PDF=20summa?= =?UTF-8?q?ry=20receiver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a regression case that simulates a failed viewer connection and verifies that the disconnect callback consumes Chrome's runtime error. Co-authored-by: Codex Web --- tests/chatgpt-summary-popup.test.mjs | 41 ++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tests/chatgpt-summary-popup.test.mjs b/tests/chatgpt-summary-popup.test.mjs index 49731e1..ff0bc2f 100644 --- a/tests/chatgpt-summary-popup.test.mjs +++ b/tests/chatgpt-summary-popup.test.mjs @@ -10,6 +10,8 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { let resolveUrl; const listeners = new Map(); const calls = []; + let onDisconnect; + let lastErrorReads = 0; const button = { title: "Summarize with ChatGPT", attributes: {}, @@ -25,10 +27,20 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { window: { screen: { availWidth: width, availHeight: height } }, resolveDocumentReferenceUrl: () => new Promise((resolve) => { resolveUrl = resolve; }), chrome: { - runtime: { connect(options) { - calls.push(["connect", options]); - return { onMessage: { addListener() {} }, onDisconnect: { addListener() {} }, disconnect() {} }; - } }, + runtime: { + get lastError() { + lastErrorReads += 1; + return { message: "Could not establish connection. Receiving end does not exist." }; + }, + connect(options) { + calls.push(["connect", options]); + return { + onMessage: { addListener() {} }, + onDisconnect: { addListener(listener) { onDisconnect = listener; } }, + disconnect() {}, + }; + }, + }, windows: { async create(options) { calls.push(["popup", options]); if (popupFails) throw new Error("Popup unavailable"); @@ -46,7 +58,15 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { }, }; vm.runInNewContext(source, context); - return { click: () => listeners.get("click")(), dispatch: (event) => listeners.get(event)(), resolveUrl, calls, button }; + return { + click: () => listeners.get("click")(), + dispatch: (event) => listeners.get(event)(), + resolveUrl, + calls, + button, + disconnect: () => onDisconnect?.(), + get lastErrorReads() { return lastErrorReads; }, + }; } test("opens the portrait popup before PDF metadata resolves, then stores the prompt before navigation", async () => { @@ -120,3 +140,14 @@ test("local file URLs are not substituted for PDF attachments", async () => { assert.ok(!data.prompt.includes("file:///Users/")); assert.equal(fixture.calls.find(([action]) => action === "connect")[1].name, "pdf-summary-source:request"); }); + +test("consumes Chrome's missing-receiver error on PDF summary disconnection", async () => { + const fixture = setup(); + const pendingClick = fixture.click(); + fixture.resolveUrl("https://www.sec.gov/files/rules/other/34-16900.pdf"); + await pendingClick; + + assert.equal(fixture.lastErrorReads, 0); + fixture.disconnect(); + assert.equal(fixture.lastErrorReads, 1); +});