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