'use strict'; const assert = require('assert'); const { _private } = require('../src/services/openai_oauth_service'); const { OAUTH_REDIRECT_URI, readOpenAIPageSnapshot, advanceOpenAIPageProgressTracker, shouldDeferOpenAIPageFailure, runWithPageCallbackCapture, } = _private; const STABLE_WAIT_MS = 2000; function progress(overrides = {}) { return { url: 'https://auth.openai.com/choose-an-account', readyState: 'complete', domSignature: 'text:20|html:120|elements:5|resources:3', busyCount: 0, ...overrides, }; } function advance(tracker, snapshot, nowMs, key = 'account-choice') { return advanceOpenAIPageProgressTracker(tracker, key, snapshot, nowMs); } function shouldDefer(tracker, nowMs) { return shouldDeferOpenAIPageFailure(tracker, nowMs, { stableMs: STABLE_WAIT_MS }); } function createCallbackPage(initialUrl) { const listeners = new Map(); const state = { url: initialUrl }; return { state, url: () => state.url, mainFrame: () => ({ url: () => state.url }), on: (name, listener) => listeners.set(name, listener), off: (name, listener) => { if (listeners.get(name) === listener) listeners.delete(name); }, waitForTimeout: async () => {}, }; } (async () => { assert.strictEqual(typeof readOpenAIPageSnapshot, 'function', '应导出页面进度读取 helper'); assert.strictEqual(typeof advanceOpenAIPageProgressTracker, 'function', '应导出页面稳定追踪 helper'); assert.strictEqual(typeof shouldDeferOpenAIPageFailure, 'function', '应导出延迟失败判断 helper'); const readState = { url: 'https://auth.openai.com/choose-an-account', snapshot: progress({ readyState: 'loading', busyCount: 1 }), }; const readPage = { url: () => readState.url, evaluate: async () => ({ ...readState.snapshot, url: undefined }), }; const readSnapshot = await readOpenAIPageSnapshot(readPage); assert.strictEqual(readSnapshot.url, readState.url, '读取结果应包含读取时的页面URL'); assert.strictEqual(readSnapshot.readyState, 'loading'); assert.strictEqual(readSnapshot.busyCount, 1); // 只要文档仍明确处于 loading,就不能因为稳定窗口已过去而宣布结构失败。 let tracker = advance(null, progress({ readyState: 'loading' }), 0); tracker = advance(tracker, progress({ readyState: 'loading' }), STABLE_WAIT_MS * 2); assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2), true, 'loading页面必须继续等待'); // DOM或资源进度变化会重置稳定计时;必须从最后一次变化重新累计。 tracker = advance(null, progress({ domSignature: 'dom-a' }), 0); tracker = advance(tracker, progress({ domSignature: 'dom-a' }), 1500); assert.strictEqual(shouldDefer(tracker, 1500), true); tracker = advance(tracker, progress({ domSignature: 'dom-b' }), 1600); assert.strictEqual(shouldDefer(tracker, 3500), true, 'DOM持续变化后不应沿用旧稳定时间'); tracker = advance(tracker, progress({ domSignature: 'dom-b' }), 3600); assert.strictEqual(shouldDefer(tracker, 3600), false, '最后一次DOM变化稳定到阈值后才允许失败'); // 即使DOM签名相同,URL变化也代表一次新页面,稳定计时必须清零。 tracker = advance(null, progress({ url: 'https://auth.openai.com/oauth/authorize' }), 0); tracker = advance(tracker, progress({ url: 'https://auth.openai.com/oauth/authorize' }), STABLE_WAIT_MS); assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS), false); tracker = advance(tracker, progress({ url: 'https://auth.openai.com/choose-an-account' }), STABLE_WAIT_MS + 1); assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2), true, 'URL变化后必须重新等待稳定窗口'); tracker = advance(tracker, progress({ url: 'https://auth.openai.com/choose-an-account' }), STABLE_WAIT_MS * 2 + 1); assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2 + 1), false); // 同一URL/DOM被用于不同页面判断时,状态key变化也必须开启新的观察窗口。 tracker = advance(null, progress({ domSignature: 'shared-dom' }), 5000, 'unknown-page'); tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 7000, 'unknown-page'); assert.strictEqual(shouldDefer(tracker, 7000), false); tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 7001, 'account-choice'); assert.strictEqual(shouldDefer(tracker, 8000), true, '状态key变化后必须重置稳定计时'); tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 9001, 'account-choice'); assert.strictEqual(shouldDefer(tracker, 9001), false); // 完成加载且URL/DOM持续不变时,阈值之前延迟,恰好达到阈值才允许终态失败。 tracker = advance(null, progress(), 10_000); tracker = advance(tracker, progress(), 10_000 + STABLE_WAIT_MS - 1); assert.strictEqual(shouldDefer(tracker, 10_000 + STABLE_WAIT_MS - 1), true); tracker = advance(tracker, progress(), 10_000 + STABLE_WAIT_MS); assert.strictEqual(shouldDefer(tracker, 10_000 + STABLE_WAIT_MS), false); // 回调可能在helper开始监听前已经落到当前URL;必须主动读取,而不能只依赖事件。 const startPage = createCallbackPage(`${OAUTH_REDIRECT_URI}?code=start-code&state=start-state`); const startCapture = await runWithPageCallbackCapture(startPage, 'start-state', async () => 'started'); assert.strictEqual(startCapture.callback && startCapture.callback.code, 'start-code'); // action结束时URL刚变成回调但没有触发mock事件,也必须通过结束主动读取捕获。 const endPage = createCallbackPage('https://auth.openai.com/oauth/authorize'); const endCapture = await runWithPageCallbackCapture(endPage, 'end-state', async () => { endPage.state.url = `${OAUTH_REDIRECT_URI}?code=end-code&state=end-state`; return 'ended'; }); assert.strictEqual(endCapture.callback && endCapture.callback.code, 'end-code'); // 账号卡片分批渲染:首帧只有其他邮箱,下一帧才出现目标邮箱。流程必须 // 继续观察,且绝不能提交非目标账号。 let accountUrl = 'https://auth.openai.com/choose-an-account'; let accountRound = 0; let targetFormReads = 0; let wrongAccountSubmitted = false; const wrongAccount = { isVisible: async () => true, innerText: async () => 'Other account\nother@gmail.com', evaluate: async () => { wrongAccountSubmitted = true; }, }; const targetAccount = { isVisible: async () => true, innerText: async () => 'Target account\ntarget@gmail.com', evaluate: async () => { targetFormReads++; if (targetFormReads === 1) { return { action: 'https://auth.openai.com/choose-an-account', method: 'POST', fields: { session_id: 'target-session' }, }; } accountUrl = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent'; return undefined; }, }; const hiddenTarget = { isVisible: async () => false, click: async () => { throw new Error('hidden target must not be clicked'); }, }; const accountPage = { url: () => accountUrl, isClosed: () => false, waitForTimeout: async () => {}, evaluate: async () => ({ readyState: accountRound < 2 ? 'loading' : 'complete', domSignature: `account-round:${accountRound}`, busyCount: accountRound < 2 ? 1 : 0, }), locator: (selector) => { if (selector === 'body') { return { innerText: async () => (accountUrl.includes('/choose-an-account') ? (accountRound < 2 ? 'Other account other@gmail.com' : 'Target account target@gmail.com') : 'OpenAI consent'), }; } if (selector === 'button[name="session_id"], button[data-dd-action-name="Select existing session"]') { accountRound++; const candidate = accountRound < 2 ? wrongAccount : targetAccount; return { count: async () => 1, nth: () => candidate }; } return { first: () => hiddenTarget }; }, }; const delayedAccount = await require('../src/services/openai_oauth_service') .loginOpenAIOAuthWithGoogle(accountPage, { email: 'target@gmail.com' }, () => {}, { pageMinimumWaitMs: 20, pageStableMs: 5, pageLoadingTimeoutMs: 100, pagePollMs: 0, }); assert.strictEqual(delayedAccount.ok, true, '目标账号稍后出现时应继续授权'); assert.strictEqual(wrongAccountSubmitted, false, '不得提交先出现的其他账号'); // OpenAI 账号选择偶发返回 Route Error 时应自动点击 Try again,最多 5 次, // 不能像旧逻辑一样第一次遇到错误就直接结束任务。 let routeErrorRetryClicks = 0; const routeErrorRetryButton = { isVisible: async () => true, click: async () => { routeErrorRetryClicks++; }, }; const routeErrorPage = { url: () => 'https://auth.openai.com/choose-an-account', isClosed: () => false, waitForTimeout: async () => {}, locator: (selector) => { if (selector === 'body') { return { innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type: text/html; charset=UTF-8)', }; } if (selector === 'button:text-is("Try again")') { return { first: () => routeErrorRetryButton }; } return { first: () => hiddenTarget }; }, }; const routeErrorResult = await require('../src/services/openai_oauth_service') .loginOpenAIOAuthWithGoogle(routeErrorPage, { email: 'target@gmail.com' }, () => {}); assert.strictEqual(routeErrorResult.ok, false); assert.strictEqual(routeErrorResult.code, 'OPENAI_ACCOUNT_SELECT_REQUEST_FAILED'); assert.strictEqual(routeErrorRetryClicks, 5, 'Route Error 的 Try again 最多只能点击 5 次'); let recoveredUrl = 'https://auth.openai.com/choose-an-account'; let recoveredRetryClicks = 0; const recoveredRetryButton = { isVisible: async () => true, click: async () => { recoveredRetryClicks++; recoveredUrl = `${OAUTH_REDIRECT_URI}?code=recovered-code&state=recovered-state`; }, }; const recoveredRouteErrorPage = { url: () => recoveredUrl, isClosed: () => false, waitForTimeout: async () => {}, locator: (selector) => { if (selector === 'body') { return { innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type)' }; } if (selector === 'button:text-is("Try again")') { return { first: () => recoveredRetryButton }; } return { first: () => hiddenTarget }; }, }; const recoveredRouteError = await require('../src/services/openai_oauth_service') .loginOpenAIOAuthWithGoogle(recoveredRouteErrorPage, { email: 'target@gmail.com' }, () => {}); assert.strictEqual(recoveredRouteError.ok, true, '点击 Try again 后恢复跳转时应继续完成授权'); assert.strictEqual(recoveredRetryClicks, 1); // 工作区正文先出现、Continue 后出现,也必须等待下一帧再点击。 let workspaceUrl = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent'; let workspaceRound = 0; let workspaceClicked = false; const workspaceButton = { isVisible: async () => true, innerText: async () => 'Continue', getAttribute: async () => null, isDisabled: async () => false, click: async () => { workspaceClicked = true; workspaceUrl = `${OAUTH_REDIRECT_URI}?code=workspace-code&state=workspace-state`; }, }; const workspacePage = { url: () => workspaceUrl, isClosed: () => false, waitForTimeout: async () => {}, evaluate: async () => ({ readyState: workspaceRound < 2 ? 'loading' : 'complete', domSignature: `workspace-round:${workspaceRound}`, busyCount: workspaceRound < 2 ? 1 : 0, }), locator: (selector) => { if (selector === 'body') { return { innerText: async () => 'Select a workspace Personal account Continue' }; } if (selector === 'button, [role="button"], input[type="submit"]') { workspaceRound++; return { count: async () => (workspaceRound < 2 ? 0 : 1), nth: () => workspaceButton, }; } return { first: () => hiddenTarget }; }, }; const delayedWorkspace = await require('../src/services/openai_oauth_service') .loginOpenAIOAuthWithGoogle(workspacePage, { email: 'target@gmail.com' }, () => {}, { pageMinimumWaitMs: 20, pageStableMs: 5, pageLoadingTimeoutMs: 100, pagePollMs: 0, }); assert.strictEqual(delayedWorkspace.ok, true, 'Continue稍后出现时应继续授权'); assert.strictEqual(workspaceClicked, true); console.log('openai_oauth_loading.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });