Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions docs/windows-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 12 additions & 12 deletions src/browser/actions/navigation.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -481,18 +481,18 @@ async function waitForPrompt(
}

async function isCloudflareInterstitial(Runtime: ChromeClient["Runtime"]): Promise<boolean> {
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);
Expand Down
1 change: 0 additions & 1 deletion src/browser/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';
Expand Down
42 changes: 41 additions & 1 deletion tests/browser/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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 = {
Expand Down
Loading