diff --git a/CHANGELOG.md b/CHANGELOG.md index 615f561da3..35b3dd696b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Fixed +- Ignore background Cloudflare scripts and unrelated notices on usable ChatGPT + pages instead of reporting a browser challenge. - Wait for a saved ChatGPT conversation after its provisional `WEB:` route, recover interrupted sessions with that route, and keep response capture and reloads within the configured timeout. diff --git a/docs/windows-work.md b/docs/windows-work.md index 6df4306eb0..d902d47471 100644 --- a/docs/windows-work.md +++ b/docs/windows-work.md @@ -69,6 +69,11 @@ Read this when working on `ask_pro` from Windows and add new findings here. Future Windows gotchas belong here. +- September 30: signed-in ChatGPT loads a `/challenge-platform/` script even + while its composer and the "Library is now Space" notice are visible. Script + presence is not a blocking challenge. Navigation only treats the Cloudflare + interstitial title as blocking when no visible enabled composer is available; + active login, MFA, and CAPTCHA surfaces remain handled by the intervention monitor. - September 10: the model picker exposes `Latest` (currently `6 Pro`). Select the literal `Latest` option and keep the reasoning slider on `Pro`. Tool work appears inside `[data-streaming-response-status]`; it is progress, not answer content. diff --git a/src/browser/actions/navigation.ts b/src/browser/actions/navigation.ts index 710345fbff..dfd4c5649c 100644 --- a/src/browser/actions/navigation.ts +++ b/src/browser/actions/navigation.ts @@ -1,5 +1,5 @@ import type { ChromeClient, BrowserLogger } from "../types.js"; -import { CLOUDFLARE_SCRIPT_SELECTOR, CLOUDFLARE_TITLE, INPUT_SELECTORS } from "../constants.js"; +import { CLOUDFLARE_TITLE, INPUT_SELECTORS } from "../constants.js"; import { delay } from "../utils.js"; import { logDomFailure } from "../domDebug.js"; import { BrowserAutomationError } from "../errors.js"; @@ -481,18 +481,18 @@ async function waitForPrompt( } async function isCloudflareInterstitial(Runtime: ChromeClient["Runtime"]): Promise { - const { result: titleResult } = await Runtime.evaluate({ - expression: "document.title", - returnByValue: true, - }); - const title = typeof titleResult.value === "string" ? titleResult.value : ""; - const challengeTitle = CLOUDFLARE_TITLE.toLowerCase(); - if (title.toLowerCase().includes(challengeTitle)) { - return true; - } - const { result } = await Runtime.evaluate({ - expression: `Boolean(document.querySelector('${CLOUDFLARE_SCRIPT_SELECTOR}'))`, + expression: `(() => { + if (!document.title.toLowerCase().includes(${JSON.stringify(CLOUDFLARE_TITLE)})) return false; + const composers = document.querySelectorAll(${JSON.stringify(INPUT_SELECTORS.join(","))}); + return !Array.from(composers).some((node) => { + if (node.hasAttribute('disabled') || node.closest('[hidden],[aria-hidden="true"],[inert]')) return false; + const style = getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + })()`, returnByValue: true, }); return Boolean(result.value); diff --git a/src/browser/constants.ts b/src/browser/constants.ts index 9151e36e74..a25819c19e 100644 --- a/src/browser/constants.ts +++ b/src/browser/constants.ts @@ -41,7 +41,6 @@ export const CONVERSATION_TURN_SELECTOR = "article[data-turn], div[data-turn], section[data-turn]"; export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]'; -export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]'; export const CLOUDFLARE_TITLE = "just a moment"; export const PROMPT_PRIMARY_SELECTOR = "#prompt-textarea"; export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]'; diff --git a/tests/browser/index.test.ts b/tests/browser/index.test.ts index ccb6813a3d..d323b0a4e7 100644 --- a/tests/browser/index.test.ts +++ b/tests/browser/index.test.ts @@ -5,7 +5,7 @@ import { runSubmissionWithRecoveryForTest, shouldPreserveBrowserOnErrorForTest, } from "../../src/browser/index.js"; -import { ensureLoggedIn } from "../../src/browser/pageActions.js"; +import { ensureLoggedIn, ensureNotBlocked } from "../../src/browser/pageActions.js"; import { BrowserAutomationError } from "../../src/browser/errors.js"; describe("shouldPreserveBrowserOnErrorForTest", () => { @@ -366,6 +366,46 @@ describe("launch tab cleanup", () => { }); }); +describe("navigation challenge detection", () => { + test.each([ + { title: "ChatGPT", composerVisible: true, scriptPresent: true, blocked: false }, + { title: "ChatGPT", composerVisible: false, scriptPresent: true, blocked: false }, + { title: "Just a moment...", composerVisible: true, scriptPresent: true, blocked: false }, + { title: "Just a moment...", composerVisible: false, scriptPresent: false, blocked: true }, + ])("$title with composerVisible=$composerVisible: blocked=$blocked", async (page) => { + const composer = { + closest: () => null, + hasAttribute: () => false, + getBoundingClientRect: () => ({ width: 548, height: page.composerVisible ? 26 : 0 }), + }; + const document = { + title: page.title, + body: { textContent: "Library is now Space" }, + querySelector: (selector: string) => + selector.startsWith("script") ? (page.scriptPresent ? {} : null) : composer, + querySelectorAll: () => [composer], + }; + const Runtime = { + evaluate: async ({ expression }: { expression: string }) => ({ + result: { + value: new Function("document", "getComputedStyle", `return ${expression}`)( + document, + () => ({ display: "block", visibility: "visible", opacity: "1" }), + ), + }, + }), + }; + const result = ensureNotBlocked(Runtime as never, false, () => {}); + if (page.blocked) { + await expect(result).rejects.toMatchObject({ + details: { stage: "cloudflare-challenge" }, + }); + } else { + await expect(result).resolves.toBeUndefined(); + } + }); +}); + describe("human intervention detection", () => { test("detects login/challenge reasons from the page probe", async () => { const Runtime = {