Files
go_bt/tests/chatgpt_service.test.js

1071 lines
40 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { loginOrRegisterChatGPT, _private } = require('../src/services/chatgpt_service');
const { RECIPES, ATOM_STEPS } = require('../src/pipeline/recipes');
const { getFailureLabel } = require('../src/utils/error_classifier');
assert.strictEqual(_private.normalizeAge(undefined), 23, '未配置年龄时应使用固定值 23');
assert.strictEqual(_private.normalizeAge(''), 23, '空年龄应使用固定值 23');
assert.strictEqual(_private.normalizeAge(17), 18, '年龄不能小于 18');
assert.strictEqual(_private.normalizeAge(120), 99, '年龄不能大于 99');
assert.strictEqual(_private.normalizeAge('35'), 35, '合法年龄应保持不变');
assert.strictEqual(_private.isAuthenticatedSession(null), false);
assert.strictEqual(_private.isAuthenticatedSession({}), false);
assert.strictEqual(_private.isAuthenticatedSession({ user: { email: 'target@example.com' } }), true);
assert.strictEqual(_private.isAuthenticatedSession({ accessToken: 'local-token' }), false, '只有Token、没有用户邮箱不能证明账号已登录');
assert.strictEqual(_private.isAuthenticatedSession({ expires: '2099-01-01T00:00:00.000Z' }), false, '只有过期时间不能证明已经登录');
assert.strictEqual(
_private.isAuthenticatedSession({ user: { email: 'target@example.com' }, expires: '2000-01-01T00:00:00.000Z' }),
false,
'已过期 Session 不能判定登录成功',
);
assert.strictEqual(
_private.sessionAccountMismatch({ user: { email: 'other@example.com' } }, 'target@example.com'),
true,
'Session 用户邮箱不一致时不能标记当前任务成功',
);
assert.strictEqual(
_private.sessionAccountMismatch({ user: { email: 'TARGET@example.com' } }, 'target@example.com'),
false,
);
assert.strictEqual(_private.normalizeIdentityEmail('0.966527426kk@gmail.com'), '0966527426kk@gmail.com');
assert.strictEqual(_private.normalizeIdentityEmail('0.966527426kk+test@googlemail.com'), '0966527426kk@gmail.com');
assert.strictEqual(_private.normalizeIdentityEmail('first.last@example.com'), 'first.last@example.com');
assert.strictEqual(_private.buildChatGPTFullName('samsung samsung1'), 'Samsung User');
assert.strictEqual(_private.buildChatGPTFullName('', 'john doe2'), 'John User');
assert.strictEqual(_private.buildChatGPTFullName('', '', '123456'), 'Alex Morgan');
assert.strictEqual(
_private.sessionAccountMismatch(
{ user: { email: '0966527426kk@gmail.com' } },
'0.966527426kk@gmail.com',
),
false,
'Gmail Session 返回无点号地址时仍应识别为同一账号',
);
const matchingConsent = 'Sign in to OpenAI target@example.com Email address';
const mismatchingConsent = 'Sign in to OpenAI another@example.com Email address';
assert.strictEqual(_private.consentAccountMismatch(matchingConsent, 'target@example.com'), false);
assert.strictEqual(_private.consentAccountMismatch(mismatchingConsent, 'target@example.com'), true);
assert.strictEqual(_private.consentAccountMismatch('Sign in to OpenAI', 'target@example.com'), false);
assert.strictEqual(
_private.consentAccountMismatch(
'Sign in to OpenAI 0966527426kk@gmail.com Email address',
'0.966527426kk@gmail.com',
),
false,
'Google 授权页中的 Gmail 无点号形式不应被误判为其它账号',
);
assert.strictEqual(_private.isOpenAIPage('https://chatgpt.com/'), true);
assert.strictEqual(_private.isOpenAIPage('https://auth.openai.com/create-account'), true);
assert.strictEqual(_private.isOpenAIPage('https://accounts.google.com/signin'), false);
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/'), true);
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/?model=auto#top'), true);
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/auth/callback?code=redacted'), false);
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/auth/error'), false);
assert.strictEqual(_private.isChatGPTAuthRoute('https://chatgpt.com/auth/callback?code=redacted'), true);
assert.strictEqual(_private.isChatGPTAuthRoute('https://chatgpt.com/'), false);
assert.strictEqual(_private.isChatGPTAuthErrorRoute('https://chatgpt.com/auth/error?error=redacted'), true);
assert.strictEqual(_private.extendDeadlineForPhase(1120, 1000, 1250), 1370);
function makeGoogleIdentifierPage({ transitionOnClick }) {
const state = {
url: 'https://accounts.google.com/v3/signin/identifier',
value: '',
inputVisible: true,
};
const input = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.inputVisible,
inputValue: async () => state.value,
fill: async value => { state.value = String(value); },
pressSequentially: async text => { state.value += String(text); },
press: async () => {},
click: async () => {},
scrollIntoViewIfNeeded: async () => {},
evaluate: async (_, value) => { state.value = String(value); },
};
const next = {
count: async () => 1,
nth() { return this; },
isVisible: async () => true,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
if (transitionOnClick) {
state.inputVisible = false;
state.url = 'https://accounts.google.com/signin/challenge/pwd';
}
},
};
const missing = {
count: async () => 0,
nth() { return this; },
};
const page = {
url: () => state.url,
waitForTimeout: async () => {},
locator: selector => {
if (selector === '#identifierId') return input;
if (selector === '#identifierNext button') return next;
return missing;
},
};
return { page, state };
}
function makeAgePage({ transitionOnClick, transitionOnAttempt, submitLabel = 'Finish creating account' }) {
const successfulAttempt = transitionOnAttempt || (transitionOnClick ? 1 : Number.POSITIVE_INFINITY);
const state = {
url: 'https://auth.openai.com/create-account',
age: '',
inputVisible: true,
clicks: 0,
};
const ageInput = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.inputVisible,
inputValue: async () => state.age,
fill: async value => { state.age = String(value); },
click: async () => {},
press: async () => {},
scrollIntoViewIfNeeded: async () => {},
evaluate: async (_, value) => { state.age = String(value); },
};
const finishButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => true,
click: async () => {
state.clicks++;
if (state.clicks >= successfulAttempt) {
state.inputVisible = false;
state.url = 'https://chatgpt.com/';
}
},
};
const missing = {
count: async () => 0,
nth() { return this; },
};
const page = {
url: () => state.url,
waitForTimeout: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => 'How old are you? Age' };
if (selector === 'input[placeholder="Age"]') return ageInput;
if (selector === `button:has-text("${submitLabel}")`) return finishButton;
return missing;
},
};
return { page, state };
}
function makeTryAnotherWayPage({ captcha = true } = {}) {
const state = {
url: captcha
? 'https://accounts.google.com/v3/signin/challenge/recaptcha'
: 'https://accounts.google.com/o/oauth2/auth',
visible: true,
clicks: 0,
};
const button = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
state.clicks++;
state.visible = false;
state.url = 'https://auth.openai.com/callback';
},
};
const missing = { count: async () => 0, nth() { return this; } };
const page = {
url: () => state.url,
waitForTimeout: async () => {},
locator: selector => {
if (selector === 'body') return {
innerText: async () => captcha
? "Verify it's you Confirm you're not a robot"
: "Verify it's you Choose another way to sign in",
};
if (selector === 'button:has-text("Try another way")') return button;
return missing;
},
};
return { page, state };
}
function makeAccountRecoveryPasswordPage() {
const state = {
url: 'https://accounts.google.com/v3/signin/challenge/pwd',
password: '',
nextClicks: 0,
alternateClicks: 0,
};
function locator(selector) {
const isPassword = selector === 'input[type="password"]';
const isNext = selector.includes('button:has-text("Next")');
const isAlternate = selector.includes('Try another way');
const element = {
count: async () => (isPassword || isNext || isAlternate ? 1 : 0),
nth() { return this; },
first() { return this; },
or() { return this; },
isVisible: async () => isPassword || isNext || isAlternate,
waitFor: async () => {},
fill: async value => { if (isPassword) state.password = String(value); },
press: async () => {},
scrollIntoViewIfNeeded: async () => {},
click: async () => {
if (isAlternate) state.alternateClicks++;
},
evaluate: async () => {
if (isNext) {
state.nextClicks++;
state.url = 'https://auth.openai.com/callback';
}
if (isAlternate) state.alternateClicks++;
return true;
},
innerText: async () => 'Account recovery Enter your password Try another way Next',
textContent: async () => '',
};
return element;
}
return {
state,
page: {
url: () => state.url,
locator,
waitForTimeout: async () => {},
waitForLoadState: async () => {},
},
};
}
function makeBirthdayPage() {
const state = {
url: 'https://auth.openai.com/create-account',
birthday: '',
inputVisible: true,
};
const birthdayInput = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.inputVisible,
inputValue: async () => state.birthday,
getAttribute: async name => (name === 'type' ? 'text' : ''),
fill: async value => { state.birthday = String(value); },
click: async () => {},
press: async () => {},
scrollIntoViewIfNeeded: async () => {},
evaluate: async (_, value) => { state.birthday = String(value); },
};
const finishButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.inputVisible,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
state.inputVisible = false;
state.url = 'https://chatgpt.com/';
},
};
const missing = { count: async () => 0, nth() { return this; } };
const page = {
url: () => state.url,
waitForTimeout: async () => {},
waitForLoadState: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
if (selector === 'input[aria-label*="Birthday" i]') return birthdayInput;
if (selector === 'button:has-text("Finish creating account")') return finishButton;
return missing;
},
};
return { page, state };
}
function makeSegmentedBirthdayPage() {
const state = {
url: 'https://auth.openai.com/create-account',
visible: true,
values: ['', '', ''],
};
const labels = ['month, Birthday', 'day, Birthday', 'year, Birthday'];
const segments = labels.map((label, index) => ({
isVisible: async () => state.visible,
getAttribute: async name => (name === 'aria-label' ? label : ''),
click: async () => {},
press: async () => {},
pressSequentially: async value => { state.values[index] = String(value); },
}));
const segmentList = {
count: async () => segments.length,
nth: index => segments[index],
};
const birthdayGroup = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
locator: selector => {
assert.strictEqual(selector, '[role="spinbutton"]');
return segmentList;
},
};
const finishButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
click: async () => {
state.visible = false;
state.url = 'https://chatgpt.com/';
},
};
const missing = { count: async () => 0, nth() { return this; } };
const page = {
url: () => state.url,
waitForTimeout: async () => {},
waitForLoadState: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
if (selector === '[role="group"][aria-label*="Birthday" i]') return birthdayGroup;
if (selector === 'button:has-text("Finish creating account")') return finishButton;
return missing;
},
};
return { page, state };
}
function makeGenericBirthdayYearPage() {
const state = {
url: 'https://auth.openai.com/create-account',
visible: true,
year: '2026',
};
const yearElement = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
textContent: async () => state.year,
innerText: async () => state.year,
getAttribute: async () => '',
click: async () => {},
press: async () => {},
pressSequentially: async value => { state.year = String(value); },
};
const finishButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
click: async () => {
state.visible = false;
state.url = 'https://chatgpt.com/';
},
};
const missing = { count: async () => 0, nth() { return this; } };
const page = {
url: () => state.url,
waitForTimeout: async () => {},
waitForLoadState: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
if (selector === '[id^="react-aria-"] *, [id^="react-aria-"]') return yearElement;
if (selector === 'button:has-text("Finish creating account")') return finishButton;
return missing;
},
};
return { page, state };
}
function makeAllSetPage() {
const state = {
url: 'https://chatgpt.com/',
visible: true,
clicks: 0,
};
const continueButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => state.visible,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
state.clicks++;
state.visible = false;
},
};
const missing = { count: async () => 0, nth() { return this; } };
const page = {
url: () => state.url,
waitForTimeout: async () => {},
waitForLoadState: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => "You're all set" };
if (selector === 'button:has-text("Continue")') return continueButton;
return missing;
},
};
return { page, state };
}
(async () => {
const email = 'target@example.com';
const signedOutState = { restarted: false, gotoCount: 0 };
const signedOutMissing = { count: async () => 0, nth() { return this; } };
const loginButton = {
count: async () => signedOutState.restarted ? 0 : 1,
nth() { return this; },
isVisible: async () => !signedOutState.restarted,
};
const signedOutPage = {
url: () => 'https://chatgpt.com/',
waitForTimeout: async () => {},
goto: async () => { signedOutState.restarted = true; signedOutState.gotoCount++; },
evaluate: async () => ({
ok: true,
status: 200,
text: signedOutState.restarted
? JSON.stringify({ user: { email }, expires: '2099-01-01T00:00:00.000Z' })
: JSON.stringify({ expires: '2099-01-01T00:00:00.000Z' }),
}),
locator: selector => {
if (selector === 'body') return { innerText: async () => signedOutState.restarted ? 'Where should we begin?' : 'Log in Sign up for free' };
if (selector.includes('Log in')) return loginButton;
return signedOutMissing;
},
};
const signedOutResult = await _private.waitForChatGPTResult(signedOutPage, { email }, 23, () => {});
assert.strictEqual(signedOutResult.ok, true, '未登录首页重新发起 Google 登录后应继续检测会话');
assert.strictEqual(signedOutState.gotoCount, 1, '检测到未登录首页后应重新进入 Google 登录入口');
const routeRaceState = {
url: 'https://chatgpt.com/',
probePages: 0,
};
const routeRaceMissing = {
count: async () => 0,
nth() { return this; },
isVisible: async () => false,
};
const routeRacePage = {
url: () => routeRaceState.url,
evaluate: async () => {
// 模拟 ChatGPT 首页读取 Session 期间,OAuth 异步跳转到 Google。
routeRaceState.url = 'https://accounts.google.com/signin/rejected';
return { ok: true, status: 200, text: '{}' };
},
context: () => ({
newPage: async () => {
routeRaceState.probePages++;
return routeRaceMissing;
},
}),
locator: selector => {
if (selector === 'body') return { innerText: async () => 'Google sign in was rejected' };
return routeRaceMissing;
},
waitForTimeout: async () => {},
};
const routeRaceResult = await _private.waitForChatGPTResult(routeRacePage, { email }, 23, () => {});
assert.strictEqual(routeRaceResult.code, 'SIGNIN_REJECTED', '页面异步跳到 Google 后应重新路由到 OAuth 处理');
assert.strictEqual(routeRaceState.probePages, 0, 'Google 页面不能被误判为首页并创建 Session 重试页');
const loginWithState = {
url: 'https://chatgpt.com/auth/login_with?callback_path=%2F',
reloads: 0,
probePages: 0,
};
const loginWithPage = {
url: () => loginWithState.url,
reload: async () => {
loginWithState.reloads++;
loginWithState.url = 'https://accounts.google.com/signin/rejected';
},
context: () => ({
newPage: async () => {
loginWithState.probePages++;
return routeRaceMissing;
},
}),
locator: selector => {
if (selector === 'body') return { innerText: async () => 'Google sign in was rejected' };
return routeRaceMissing;
},
waitForTimeout: async () => {},
};
const loginWithResult = await _private.waitForChatGPTResult(loginWithPage, { email }, 23, () => {});
assert.strictEqual(loginWithResult.code, 'SIGNIN_REJECTED', '登录中转页停滞时应刷新并按新的 Google 路由继续处理');
assert.strictEqual(loginWithState.reloads, 1, '登录中转页停滞时应主动刷新');
assert.strictEqual(loginWithState.probePages, 0, '登录中转页不能被误判为 ChatGPT 首页并创建 Session 重试页');
const loadingLoginWithState = {
url: 'https://chatgpt.com/auth/login_with?callback_path=%2F&connection=google-oauth2',
ticks: 0,
reloads: 0,
};
const loadingLoginWithPage = {
url: () => loadingLoginWithState.url,
reload: async () => { loadingLoginWithState.reloads++; },
evaluate: async () => ({
available: true,
readyState: 'loading',
bodyTextLength: 0,
bodyHtmlLength: 20,
bodyElementCount: 1,
resourceCount: loadingLoginWithState.ticks >= 20 ? 1 : 0,
resourceTransferSize: loadingLoginWithState.ticks >= 20 ? 1024 : 0,
navigationResponseEnd: 0,
}),
locator: selector => {
if (selector === 'body') {
return {
innerText: async () => loadingLoginWithState.url.includes('signin/rejected')
? 'Google sign in was rejected'
: '',
};
}
return routeRaceMissing;
},
waitForTimeout: async () => {
loadingLoginWithState.ticks++;
if (loadingLoginWithState.ticks === 50) {
loadingLoginWithState.url = 'https://accounts.google.com/signin/rejected';
}
},
};
const loadingLoginWithResult = await _private.waitForChatGPTResult(loadingLoginWithPage, { email }, 23, () => {});
assert.strictEqual(loadingLoginWithResult.code, 'SIGNIN_REJECTED', '登录中转页仍有加载进展时应继续等待真实路由');
assert.strictEqual(loadingLoginWithState.reloads, 0, '网络较慢但页面仍在加载时不能误刷新');
const existingLoginState = { url: 'https://chatgpt.com/', mainNavigations: [], probeOpened: 0 };
const existingLoginPage = {
url: () => existingLoginState.url,
goto: async url => {
existingLoginState.mainNavigations.push(url);
existingLoginState.url = url;
},
evaluate: async () => ({ ok: true, status: 200, text: '{}' }),
locator: selector => {
if (selector === 'body') {
return { innerText: async () => 'ChatGPT New chat Images Library Upgrade Free' };
}
return routeRaceMissing;
},
context: () => ({
newPage: async () => {
existingLoginState.probeOpened++;
return {
goto: async () => {},
locator: () => ({
innerText: async () => JSON.stringify({
user: { email },
expires: '2099-01-01T00:00:00.000Z',
}),
}),
close: async () => {},
};
},
}),
waitForTimeout: async () => {},
};
const existingLoginResult = await loginOrRegisterChatGPT(existingLoginPage, { email }, () => {});
assert.strictEqual(existingLoginResult.ok, true, '已登录首页首次读取失败后应继续提取 Session');
assert.strictEqual(existingLoginState.probeOpened, 1);
assert.strictEqual(existingLoginState.mainNavigations.some(url => /login_with/.test(url)), false, '已登录首页不能重新走 Google OAuth');
const unknownHomeState = { url: 'https://chatgpt.com/', mainNavigations: [], probeOpened: 0 };
const unknownHomePage = {
url: () => unknownHomeState.url,
goto: async url => {
unknownHomeState.mainNavigations.push(url);
unknownHomeState.url = url;
},
evaluate: async () => ({ ok: true, status: 200, text: '{}' }),
locator: selector => {
if (selector === 'body') {
return { innerText: async () => "What's on the agenda today?" };
}
return routeRaceMissing;
},
context: () => ({
newPage: async () => {
unknownHomeState.probeOpened++;
return {
goto: async () => ({ status: () => 200, headers: async () => ({ 'content-type': 'application/json' }) }),
locator: () => ({
innerText: async () => JSON.stringify({
user: { email },
expires: '2099-01-01T00:00:00.000Z',
}),
}),
close: async () => {},
};
},
}),
waitForTimeout: async () => {},
};
const unknownHomeResult = await loginOrRegisterChatGPT(unknownHomePage, { email }, () => {});
assert.strictEqual(unknownHomeResult.ok, true, '主页文案变更但未明确退出时仍应补试 Session');
assert.strictEqual(unknownHomeState.probeOpened, 1);
assert.strictEqual(
unknownHomeState.mainNavigations.some(url => /login_with/.test(url)),
false,
'未知主页布局不能直接重走 Google OAuth',
);
const routeErrorPage = {
locator: selector => {
assert.strictEqual(selector, 'body');
return { innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type: text/html; charset=UTF-8)' };
},
};
assert.strictEqual(await _private.detectChatGPTRouteError(routeErrorPage), true);
const authRouteMissing = {
count: async () => 0,
nth() { return this; },
isVisible: async () => false,
};
const authErrorPage = {
url: () => 'https://chatgpt.com/auth/error?error=redacted',
locator: selector => selector === 'body'
? { innerText: async () => '' }
: authRouteMissing,
waitForTimeout: async () => {},
};
const authErrorResult = await _private.waitForChatGPTResult(authErrorPage, { email }, 23, () => {});
assert.strictEqual(authErrorResult.code, 'CHATGPT_AUTH_ROUTE_ERROR');
assert.strictEqual(authErrorResult.restartBrowser, true);
const callbackState = { url: 'https://chatgpt.com/auth/callback?code=redacted' };
const callbackPage = {
url: () => callbackState.url,
locator: selector => selector === 'body'
? { innerText: async () => '' }
: authRouteMissing,
waitForTimeout: async () => { callbackState.url = 'https://chatgpt.com/'; },
evaluate: async () => ({
ok: true,
status: 200,
text: JSON.stringify({ user: { email }, expires: '2099-01-01T00:00:00.000Z' }),
}),
};
const callbackResult = await _private.waitForChatGPTResult(callbackPage, { email }, 23, () => {});
assert.strictEqual(callbackResult.ok, true, '授权回调路由应等待落到真正首页后再读取 Session');
const captchaPage = makeTryAnotherWayPage();
const captchaResult = await _private.handleGoogleOAuth(
captchaPage.page,
{ email },
() => {},
{ captchaWaitMs: 1 },
);
assert.strictEqual(captchaResult.ok, false, '明确的人机验证页等待超时后应失败');
assert.strictEqual(captchaResult.code, 'CAPTCHA');
assert.strictEqual(captchaPage.state.clicks, 0, '人机验证页不能点击 Try another way');
const clearedCaptchaState = {
url: 'https://accounts.google.com/v3/signin/challenge/recaptcha',
waits: 0,
};
const clearedCaptchaMissing = { count: async () => 0, nth() { return this; } };
const clearedCaptchaPage = {
url: () => clearedCaptchaState.url,
locator: selector => selector === 'body'
? { innerText: async () => "Verify it's you Confirm you're not a robot" }
: clearedCaptchaMissing,
waitForTimeout: async () => {
clearedCaptchaState.waits++;
if (clearedCaptchaState.waits >= 2) {
clearedCaptchaState.url = 'https://auth.openai.com/callback';
}
},
};
let captchaDetected = 0;
let captchaCleared = 0;
const clearedCaptchaResult = await _private.handleGoogleOAuth(
clearedCaptchaPage,
{ email },
() => {},
{
captchaWaitMs: 5000,
onCaptchaChallenge: () => { captchaDetected++; },
onCaptchaCleared: () => { captchaCleared++; },
},
);
assert.strictEqual(clearedCaptchaResult.ok, true,
'用户手动完成人机验证后应自动继续 OAuth 流程');
assert.strictEqual(captchaDetected, 1);
assert.strictEqual(captchaCleared, 1);
const staleChooserState = {
url: 'https://accounts.google.com/v3/signin/challenge/recaptcha?continue=redacted',
waits: 0,
accountClicks: 0,
};
const staleChooserMissing = { count: async () => 0, nth() { return this; } };
const staleChooserAccount = {
count: async () => 1,
nth() { return this; },
isVisible: async () => true,
getAttribute: async name => name === 'data-identifier' ? email : '',
innerText: async () => email,
click: async () => { staleChooserState.accountClicks++; },
};
const staleChooserPage = {
url: () => staleChooserState.url,
locator: selector => {
if (selector === 'body') return { innerText: async () => `Choose an account ${email} Use another account` };
if (selector === '[data-identifier]') return staleChooserAccount;
return staleChooserMissing;
},
waitForTimeout: async () => {
staleChooserState.waits++;
if (staleChooserState.waits >= 2) staleChooserState.url = 'https://auth.openai.com/callback';
},
};
const staleChooserLogs = [];
const staleChooserResult = await _private.handleGoogleOAuth(
staleChooserPage,
{ email },
message => staleChooserLogs.push(message),
);
assert.strictEqual(staleChooserResult.ok, true,
'recaptcha 路由仍显示旧账号选择页时应等待真实页面,不得误判 CAPTCHA');
assert.strictEqual(staleChooserState.accountClicks, 0,
'网址已进入验证路由后不得重复点击旧账号卡片');
assert(staleChooserLogs.some(message => message.includes('验证路由正在加载')));
const accountRecovery = makeAccountRecoveryPasswordPage();
const recoveryResult = await _private.handleGoogleOAuth(
accountRecovery.page,
{ email, password: 'test-password' },
() => {},
);
assert.strictEqual(recoveryResult.ok, true, 'Account recovery 密码页应完成密码提交');
assert.strictEqual(accountRecovery.state.password, 'test-password');
assert.strictEqual(accountRecovery.state.nextClicks, 1);
assert.strictEqual(accountRecovery.state.alternateClicks, 0, '存在密码框时不能优先点击 Try another way');
const alternateWay = makeTryAnotherWayPage({ captcha: false });
const alternateLogs = [];
const alternateResult = await _private.handleGoogleOAuth(
alternateWay.page,
{ email },
message => alternateLogs.push(message),
);
assert.strictEqual(alternateResult.ok, true, '非人机验证页仍应支持 Try another way');
assert.strictEqual(alternateWay.state.clicks, 1, '非人机验证页应点击 Try another way');
assert(alternateLogs.some(message => message.includes('Try another way')));
const successful = makeGoogleIdentifierPage({ transitionOnClick: true });
const successResult = await _private.fillGoogleIdentifier(
successful.page,
email,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(successResult.submitted, true, '点击 Next 并离开邮箱页后应判定为提交成功');
assert.strictEqual(successful.state.value, email, '提交前必须完整写入邮箱');
const stuck = makeGoogleIdentifierPage({ transitionOnClick: false });
const stuckResult = await _private.fillGoogleIdentifier(
stuck.page,
email,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(stuckResult.submitted, false, '点击后仍停留在邮箱页时不能误判为成功');
assert.match(stuckResult.msg, /仍停留/);
const ageSuccess = makeAgePage({ transitionOnClick: true });
const ageSuccessResult = await _private.fillAgeOnboarding(
ageSuccess.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(ageSuccessResult.ok, true, '年龄提交后离开页面才应判定成功');
assert.strictEqual(ageSuccess.state.age, '23');
const continueAge = makeAgePage({ transitionOnClick: true, submitLabel: 'Continue' });
const continueAgeResult = await _private.fillAgeOnboarding(
continueAge.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(continueAgeResult.ok, true, '年龄页按钮为 Continue 时也应提交成功');
const retriedAge = makeAgePage({ transitionOnAttempt: 3 });
const retriedAgeResult = await _private.fillAgeOnboarding(
retriedAge.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(retriedAgeResult.ok, true, '年龄页首次点击无效时应自动重试');
assert.strictEqual(retriedAge.state.clicks, 3);
const birthday = makeBirthdayPage();
const birthdayResult = await _private.fillAgeOnboarding(
birthday.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(birthdayResult.ok, true, '新版 Birthday 页面应提交成功');
assert.strictEqual(birthday.state.birthday, _private.birthdayForAge(23).local);
assert.strictEqual(_private.birthdayForAge(23).year, '2000', '生日页应固定使用 2000 年');
const segmentedBirthday = makeSegmentedBirthdayPage();
const segmentedBirthdayResult = await _private.fillAgeOnboarding(
segmentedBirthday.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
const expectedBirthday = _private.birthdayForAge(23);
assert.strictEqual(segmentedBirthdayResult.ok, true, 'React Aria 分段生日控件应提交成功');
assert.deepStrictEqual(segmentedBirthday.state.values, [
'',
'',
expectedBirthday.year,
]);
const genericBirthday = makeGenericBirthdayYearPage();
const genericBirthdayResult = await _private.fillAgeOnboarding(
genericBirthday.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(genericBirthdayResult.ok, true, '普通 React Aria 年份 div 应能提交');
assert.strictEqual(genericBirthday.state.year, '2000', '应把可见年份 2026 替换成 2000');
const allSet = makeAllSetPage();
const allSetResult = await _private.completeAllSetPage(
allSet.page,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(allSetResult.ok, true, "You're all set 页面应点击 Continue");
assert.strictEqual(allSet.state.clicks, 1);
const directAllSet = makeAllSetPage();
const directProbeState = { opened: 0, closed: 0, url: '' };
directAllSet.page.context = () => ({
newPage: async () => {
directProbeState.opened++;
return {
goto: async url => { directProbeState.url = url; },
locator: selector => {
assert.strictEqual(selector, 'body');
return {
innerText: async () => JSON.stringify({
user: { email: 'target@example.com' },
expires: '2099-01-01T00:00:00.000Z',
}),
};
},
close: async () => { directProbeState.closed++; },
};
},
});
const directAllSetResult = await _private.waitForChatGPTResult(
directAllSet.page,
{ email: 'target@example.com' },
23,
() => {},
);
assert.strictEqual(directAllSetResult.ok, true, "You're all set 页面应直接提取 Session");
assert.strictEqual(directAllSetResult.status, '已注册');
assert.strictEqual(directAllSet.state.clicks, 0, '直接提取成功后不应再依赖 Continue 跳转');
assert.strictEqual(directProbeState.opened, 1);
assert.strictEqual(directProbeState.closed, 1, 'Session 临时页必须及时关闭');
assert.strictEqual(directProbeState.url, 'https://chatgpt.com/api/auth/session');
const throttledAllSet = makeAllSetPage();
throttledAllSet.page.context = () => ({
newPage: async () => ({
goto: async () => ({
status: () => 429,
headers: async () => ({ 'content-type': 'application/json' }),
}),
locator: () => ({ innerText: async () => '{}' }),
close: async () => {},
}),
});
const throttledAllSetResult = await _private.waitForChatGPTResult(
throttledAllSet.page,
{ email: 'target@example.com' },
23,
() => {},
);
assert.strictEqual(throttledAllSetResult.code, 'CHATGPT_SESSION_HTTP_429');
assert.strictEqual(throttledAllSet.state.clicks, 0, 'Session 接口明确限流时不应覆盖为 Continue 或通用超时错误');
const retryProbeState = { attempts: 0, closed: 0, timeouts: [], retryDelays: [] };
const retryProbePage = {
context: () => ({
newPage: async () => ({
goto: async (_url, options) => {
retryProbeState.attempts++;
retryProbeState.timeouts.push(options.timeout);
if (retryProbeState.attempts < 5) throw new Error('temporary network error');
},
locator: () => ({
innerText: async () => JSON.stringify({
user: { email: 'target@example.com' },
expires: '2099-01-01T00:00:00.000Z',
}),
}),
close: async () => { retryProbeState.closed++; },
}),
}),
waitForTimeout: async ms => { retryProbeState.retryDelays.push(ms); },
};
const fifthAttemptSession = await _private.readChatGPTSessionInProbePage(
retryProbePage,
() => {},
);
assert.strictEqual(_private.isAuthenticatedSession(fifthAttemptSession), true, '前4次失败后第5次成功仍应提取 Session');
assert.strictEqual(retryProbeState.attempts, 5, 'Session 最多应尝试5次');
assert.deepStrictEqual(retryProbeState.timeouts, [10000, 10000, 10000, 10000, 10000], '每次读取超时应为10秒');
assert.strictEqual(retryProbeState.retryDelays.length, 4, '5次请求之间应等待4次');
assert(retryProbeState.retryDelays.every(ms => ms >= 9000), '快速返回空Session时,两次请求起点至少应间隔约10秒');
assert.strictEqual(retryProbeState.closed, 1);
const navigationState = { url: 'https://chatgpt.com/' };
const navigationPage = {
url: () => navigationState.url,
goto: async url => { navigationState.url = url; },
locator: selector => {
assert.strictEqual(selector, 'body');
return { innerText: async () => JSON.stringify({ user: { email: 'target@example.com' } }) };
},
};
const navigationSession = await _private.readChatGPTSessionByNavigation(navigationPage, () => {});
assert.strictEqual(_private.isAuthenticatedSession(navigationSession), true);
assert.strictEqual(navigationState.url, 'https://chatgpt.com/', '读取 Session 后应返回 ChatGPT 首页');
const guestState = { authenticated: false, url: 'https://chatgpt.com/', loginStarts: 0 };
const emptyLocator = {
count: async () => 0,
nth() { return this; },
isVisible: async () => false,
};
const guestPage = {
url: () => guestState.url,
evaluate: async () => ({
ok: true,
status: 200,
text: JSON.stringify(guestState.authenticated
? { user: { email: 'target@example.com' }, expires: '2099-01-01T00:00:00.000Z' }
: {}),
}),
locator: selector => {
if (selector === 'body') {
return {
innerText: async () => guestState.authenticated
? 'Where should we begin?'
: 'Where should we begin? Sign up for free Get responses tailored to you Log in to get answers based on saved chats',
};
}
return emptyLocator;
},
goto: async url => {
assert.match(url, /login_with/);
guestState.loginStarts++;
guestState.authenticated = true;
guestState.url = 'https://chatgpt.com/';
},
waitForTimeout: async () => {},
};
assert.strictEqual(await _private.isSignedOutChatGPTHome(guestPage), true, '游客首页正文也必须被识别');
const guestRecovery = await _private.waitForChatGPTResult(
guestPage,
{ email: 'target@example.com' },
23,
() => {},
);
assert.strictEqual(guestState.loginStarts, 1, '游客首页必须重新发起 Google 登录');
assert.strictEqual(guestRecovery.ok, true);
assert.strictEqual(guestRecovery.session.user.email, 'target@example.com');
const ageStuck = makeAgePage({ transitionOnClick: false });
const ageStuckResult = await _private.fillAgeOnboarding(
ageStuck.page,
{ email: 'target@example.com' },
23,
() => {},
{ transitionTimeout: 5 },
);
assert.strictEqual(ageStuckResult.ok, false, '年龄页未离开时不能误判为成功');
assert.strictEqual(ageStuckResult.retryable, true);
assert.strictEqual(ageStuckResult.code, 'CHATGPT_AGE_SUBMIT_STUCK');
const realDateNow = Date.now;
let virtualNow = 0;
try {
Date.now = () => virtualNow;
const deadlineAge = makeAgePage({ transitionOnClick: false });
deadlineAge.page.waitForTimeout = async ms => {
virtualNow += Math.max(1, Number(ms) || 0) * 10;
};
const deadlineAgeResult = await _private.waitForChatGPTResult(
deadlineAge.page,
{ email },
23,
() => {},
);
assert.strictEqual(
deadlineAgeResult.code,
'CHATGPT_AGE_SUBMIT_STUCK',
'年龄阶段自身耗时超过总截止时间时应保留明确阶段错误,不能折叠为 Session 超时',
);
} finally {
Date.now = realDateNow;
}
console.log('chatgpt_service.test.js: PASS');
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
assert.ok(Array.isArray(RECIPES.chatgpt_register));
assert.strictEqual(RECIPES.chatgpt_register.length, 1);
assert.strictEqual(typeof RECIPES.chatgpt_register[0], 'function');
assert.strictEqual(ATOM_STEPS.chatgpt_register.step, RECIPES.chatgpt_register[0]);
assert.strictEqual(getFailureLabel('CHATGPT_SESSION_TIMEOUT'), 'ChatGPT失败');
assert.strictEqual(getFailureLabel('CHATGPT_AGE_INPUT_FAILED'), 'ChatGPT失败');
assert.strictEqual(getFailureLabel('CHATGPT_AUTH_ROUTE_ERROR'), 'ChatGPT失败');
const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
assert.match(html, /ChatGPT登录\/注册/);
assert.match(html, /ChatGPT 注册年龄/);
assert.match(html, /dashboardStats\.tierCounts\.level3\['ChatGPT成功'\]/);
assert.match(html, /dashboardStats\.tierCounts\.level3\['ChatGPT失败'\]/);
assert.match(appSource, /runTask\('chatgpt_register'/);
assert.match(appSource, /chatgpt_status/);