Files
go_bt/tests/openai_oauth_loading.test.js
T

294 lines
12 KiB
JavaScript

'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;
});